def clore_commission(self): """ Cloture la commission """ # Objectif : récolter les fichiers comm en fin de commission, calculer tous les scores finals, classés les # candidats et reconstituer un fichier unique par filière (class_XXX.xml). Enfin, construire des tableaux *.csv # nécessaires à la suite du traitement administratif du recrutement (ces tableaux sont définis dans config.py) for fil in filieres: # pour chaque filière path = os.path.join(os.curdir, "data", "comm_{}*.xml".format(fil.upper())) list_fich = [Fichier(fich) for fich in glob.glob(path)] # récupération des fichiers comm de la filière list_fich = sorted(list_fich, key = lambda fich: fich.nom) # l'ordre est important pour la suite list_doss = [] # contiendra les dossiers de chaque sous-comm # Pour chaque sous-commission for fich in list_fich: # Les fichiers non vus se voient devenir NC, score final = 0, avec # motifs = "Dossier moins bon que le dernier classé" (sauf s'il y a déjà un motif - Admin) for c in fich: if Fichier.get(c, 'traité') != 'oui': Fichier.set(c, 'Correction', 'NC') Fichier.set(c, 'Score final', '0') if Fichier.get(c, 'Jury') == 'Auto': # 'Auto' est la valeur par défaut du champ-xml 'Jury' Fichier.set(c, 'Motifs', 'Dossier moins bon que le dernier classé.') # list_doss récupère la liste des dossiers classée selon score_final + age list_doss.append(fich.ordonne('score_f')) # Ensuite, on entremêle les dossiers de chaque sous-comm doss_fin = [] # contiendra les dossiers intercalés comme il se doit.. if list_doss: # Y a-t-il des dossiers dans cette liste ? nb = len(list_doss[0]) # (taille du fichier du 1er jury de cette filière) num = 0 for i in range(nb): # list_doss[0] est le plus grand !! doss_fin.append(list_doss[0][i]) for k in range(1, len(list_doss)): # reste-t-il des candidats classés dans les listes suivantes ? if i < len(list_doss[k]): doss_fin.append(list_doss[k][i]) res = etree.Element('candidats') # Création d'une arborescence xml 'candidats' [res.append(c) for c in doss_fin] # qu'on remplit avec les candidats classés. # Calcul et renseignement du rang final (index dans res) rg = 1 for cand in res: nu = 'NC' if Fichier.get(cand, 'Correction') != 'NC': # si le candidat est classé nu = str(rg) rg += 1 Fichier.set(cand, 'Rang final', nu) # rang final = NC si non classé # Sauvegarde du fichier class... nom = os.path.join(os.curdir, "data", "class_{}.xml".format(fil.upper())) with open(nom, 'wb') as fichier: fichier.write(etree.tostring(res, pretty_print=True, encoding='utf-8')) # On lance la génération des tableaux bilan de commission list_fich = [Fichier(fich) for fich in glob.glob(os.path.join(os.curdir, "data", "class_*.xml"))] self.tableaux_bilan(list_fich)
def choix_comm(self, **kwargs): """ Appelée quand un jury sélectionne un fichier dans son menu. Retourne la page de traitement de ce dossier. """ # récupère le client client = self.get_client_cour( ) # quel jury ? (sur quel machine ? on le sait grâce au cookie) # Teste si le fichier n'a pas été choisi par un autre jury fichier = kwargs.get( "fichier") # nom du fichier sélectionné par le jury a = fichier in self.fichiers_utilises.values() b = fichier != self.fichiers_utilises.get(client, 'rien') if (a and b): # Si oui, retour menu return self.affiche_menu() else: # sinon, mise à jour des attributs du client : l'attribut fichier du client va recevoir une instance d'un # objet Fichier, construit à partir du nom de fichier. client.set_fichier(Fichier(fichier)) # Mise à jour de la liste des fichiers utilisés self.fichiers_utilises[client] = fichier # On émet un message SSE : ajout d'un fichier à la liste des fichiers en cours de traitement self.add_sse_message('add', fichier) # mem_scroll initialisé : cookie qui stocke la position de l'ascenseur dans la liste des dossiers cherrypy.session['mem_scroll'] = '0' # Affichage de la page de gestion des dossiers return self.affi_dossier()
def page_impression(self, **kwargs): """ Appelée par l'admin (2e menu, clique sur un fichier class_XXX.xml). Lance le menu d'impression des fiches bilan de commission. Retourne la page impression (elle ne contient que le bouton 'RETOUR' (merci le css). """ client = self.get_client_cour( ) # récupère le client (c'est un admin !) # Mise à jour des attributs du client client.set_fichier( Fichier(kwargs["fichier"]) ) # son fichier courant devient celui qu'il vient de choisir return self.html_compose.page_impression(client)
def genere_liste_stat(self, qui): """ Sous-fonction pour le menu admin : affichage des statistiques de candidatures """ liste_stat = '' if len(glob.glob(os.path.join( os.curdir, "data", "admin_*.xml"))) > 0: # si les fichiers admin existent # lecture du fichier stat chem = os.path.join(os.curdir, "data", "stat") if not ( os.path.exists(chem) ): # le fichier stat n'existe pas (cela ne devrait pas arriver) # on le créé list_fich = [ Fichier(fich) for fich in glob.glob( os.path.join(os.curdir, "data", "admin_*.xml")) ] qui.stat() # maintenant on peut effectivement lire le fichier stat with open(os.path.join(os.curdir, "data", "stat"), 'br') as fich: stat = pickle.load(fich) # Création de la liste à afficher liste_stat = '<h4>Statistiques : {} candidats dont {} ayant validé.</h4>'.format( stat['nb_cand'], stat['nb_cand_valid']) # Pour commencer les sommes par filières liste_stat += '<ul style = "margin-top:-5%">' deja_fait = [0 ] # sert au test ci-dessous si on n'a pas math.log2() for i in range(len(filieres)): liste_stat += '<li>{} dossiers {} validés</li>'.format( stat[2**i], filieres[i].upper()) deja_fait.append(2**i) # Ensuite les requêtes croisées liste_stat += 'dont :<ul>' for i in range(2**len(filieres)): if not ( i in deja_fait ): # avec la fonction math.log2 ce test serait facile !!! seq = [] bina = bin( i )[2:] # bin revoie une chaine qui commence par 'Ob' : on vire ! while len(bina) < len(filieres): bina = '0{}'.format( bina) # les 0 de poids fort sont restaurés for char in range(len(bina)): if bina[char] == '1': seq.append(filieres[len(filieres) - char - 1].upper()) txt = ' + '.join(seq) liste_stat += '<li>{} dossiers {}</li>'.format( stat[i], txt) liste_stat += '</ul></ul>' return liste_stat
def stat(self): """ Effectue des statistiques sur les candidats """ # Récupère la liste des fichiers concernés list_fichiers = [Fichier(fich) for fich in glob.glob(os.path.join(os.curdir, "data", "admin_*.xml"))] # On ordonne la liste de fichiers transmise selon l'ordre spécifié dans filieres (parametres.py) list_fich = sorted(list_fichiers, key = lambda f: filieres.index(f.filiere().lower())) # L'info de candidatures est stockée dans un mot binaire où 1 bit # correspond à 1 filière. Un dictionnaire 'candid' admet ces mots binaires pour clés, # et les valeurs sont des nombres de candidats. # candid = {'001' : 609, '011' : 245, ...} indique que 609 candidats ont demandé # la filière 1 et 245 ont demandé à la fois la filière 1 et la filière 2 # Initialisation du dictionnaire stockant toutes les candidatures candid = {i : 0 for i in range(2**len(filieres))} # Variables de décompte des candidats (et pas candidatures !) candidats = 0 candidats_ayant_valide = 0 # Recherche des candidatures # je suis très fier de cet algorithme !! # Construction des éléments de recherche l_dict = [ {Fichier.get(cand, 'Num ParcoursSup') : cand for cand in fich} for fich in list_fich ] # liste de dicos l_set = [ set(d.keys()) for d in l_dict ] # list d'ensembles (set()) d'identifiants ParcoursSup # Création des statistiques for (k,n) in enumerate(l_set): # k = index filière ; n = ensemble des identifiants des candidats dans la filière while len(n) > 0: # tant qu'il reste des identifiants dans n a = n.pop() # on en prélève 1 (et il disparait de n) candidats += 1 cc, liste = 2**k, [k] # filière k : bit de poids 2**k au niveau haut. for i in range(k+1, len(list_fich)): # on cherche cet identifiant dans les autres filières. if a in l_set[i]: # s'il y est : cc |= 2**i # on met le bit 2**i au niveau haut (un ou exclusif est parfait) l_set[i].remove(a) # on supprime cet identifiant de l'ensemble des identifiants de la filière i liste.append(i) # on ajoute la filière i à la liste des filières demandées par le candidat [Fichier.set(l_dict[j][a], 'Candidatures', cc) for j in liste] # On écrit le noeud 'Candidatures' flag = True # pour ne compter qu'une validation par candidat ! for j in liste: # le test ci-dessous pourrait exclure les filières inadéquates (bien ou pas ?).. if not('non validée' in Fichier.get(l_dict[j][a], 'Motifs')): candid[2**j]+= 1 # ne sont comptés que les candidatures validées if flag: candidats_ayant_valide += 1 flag = False if len(liste) > 1: # si candidat dans plus d'une filière candid[cc] += 1 # incrémentation du compteur correspondant # Sauvegarder [fich.sauvegarde() for fich in list_fich] # Ajouter deux éléments dans le dictionnaire candid candid['nb_cand'] = candidats candid['nb_cand_valid'] = candidats_ayant_valide # Écrire le fichier stat with open(os.path.join(os.curdir, "data", "stat"), 'wb') as stat_fich: pickle.dump(candid, stat_fich) return
def choix_admin(self, **kwargs): """ Appelée quand l'admin sélectionne un fichier 'admin_XXX.xml' dans son premier menu. Retourne la page de traitement de ce dossier. """ cherrypy.response.headers["content-type"] = "text/html" # récupère le client client = self.get_client_cour( ) # quel client ? (sur quel machine ? on le sait grâce au cookie) # Mise à jour des attributs du client : l'attribut fichier du client va recevoir une instance d'un objet # Fichier, construit à partir du nom de fichier. client.set_fichier(Fichier(kwargs["fichier"])) ## Initialisation des paramètres # mem_scroll : cookie qui stocke la position de l'ascenseur dans la liste des dossiers cherrypy.session['mem_scroll'] = '0' # Affichage de la page de gestion des dossiers return self.affi_dossier()
def generation_comm(self): """ Création des fichiers commission """ # Objectif : classer les candidats (fichier admin) par ordre de score brut décroissant et générer autant de # fichiers qu'il y a de jurys dans la filière concernées. Ces fichiers sont construits de façon à ce qu'ils # contiennent des candidatures également solides. # Récupération des fichiers admin list_fich = [Fichier(fich) for fich in glob.glob(os.path.join(os.curdir, "data", "admin_*.xml"))] # Pour chaque fichier "admin_*.xml" for fich in list_fich: # Tout d'abord, calculer (et renseigner le noeud) le score brut de chaque candidat for cand in fich: Fichier.calcul_scoreb(cand) # Classement par scoreb décroissant doss = fich.ordonne('score_b') # Calcul du rang de chaque candidat et renseignement du noeuds 'rang_brut' for cand in fich: Fichier.set(cand, 'Rang brut', str(Fichier.rang(cand, doss, 'Score brut'))) # Récupération de la filière et du nombre de jurys nbjury = int(nb_jurys[fich.filiere().lower()]) # Découpage en n listes de dossiers for j in range(nbjury): dossier = [] # deepcopy ligne suivante sinon les candidats sont retirés de doss à chaque append [dossier.append(copy.deepcopy(doss[i])) for i in range(len(doss)) if i%nbjury == j] # Sauvegarde dans un fichier comm_XXXX.xml res = etree.Element('candidats') [res.append(cand) for cand in dossier] nom = os.path.join(os.curdir, "data", "comm_{}{}.xml".format(fich.filiere().upper(), j+1)) with open(nom, 'wb') as fichier: fichier.write(etree.tostring(res, pretty_print=True, encoding='utf-8')) # Création fichier decompte : celui-ci contiendra en fin de commission le nombre de candidats traités pour # chacune des filières. Ici, il est créé et initialisé. Il contient un dictionnaire {'filière' : nb, ...} decompt = {} for fil in filieres: decompt['{}'.format(fil.upper())] = 0 with open(os.path.join(os.curdir, "data", "decomptes"), 'wb') as stat_fich: pickle.dump(decompt, stat_fich)
def menu_admin(self, qui, fichiers_utilises, comm_en_cours): """ Compose le menu administrateur contenu : selon l'état (phase 1, 2 ou 3) du traitement phase 1 : avant la commission, l'admin gère ce qui provient de ParcoursSup, commente et/ou complète les dossiers phase 2 : l'admin a généré les fichiers *_comm_* destinés à la commission. Les différents jurys doivent se prononcer sur les dossiers. C'est le coeur de l'opération de sélection. phase 3 : commission terminée. L'admin doit gérer "l'après sélection" : recomposer un fichier ordonné par filière, générer tous les tableaux récapitulatifs. """ data = {} ## entête page = self.genere_entete('{} - Accès {}.'.format( self.titre, qui.get_droits())) list_fich_comm = glob.glob( os.path.join(os.curdir, "data", "comm_*.xml")) patron = 'menu_admin_' if len(list_fich_comm) > 0: # phase 2 ou 3 data['decompt'] = self.genere_liste_decompte() data['liste_stat'] = self.genere_liste_stat(qui) if comm_en_cours: # phase 2 patron += 'pendant' txt = '' for fich in fichiers_utilises.values(): txt += '<input type = "submit" class ="fichier" name = "fichier" value = "{}"/><br>'.format( fich) data['liste_jurys'] = txt else: # phase 3 patron += 'apres' # Etape 4 bouton data['bout_etap4'] = '<input type = "button" class ="fichier"' data[ 'bout_etap4'] += ' value = "Récolter les fichiers" onclick = "recolt_wait();"/>' # Etape 5 bouton et Etape 6 list_fich_class = glob.glob( os.path.join(os.curdir, "data", "class_*.xml")) data['liste_impression'] = '' if len(list_fich_class) > 0: data['liste_impression'] = self.genere_liste_impression() else: # avant commission patron += 'avant' # liste csv data['liste_csv'] = self.genere_liste_csv() # liste pdf data['liste_pdf'] = self.genere_liste_pdf() # liste admin data['liste_admin'] = self.genere_liste_admin() # liste_stat data['liste_stat'] = self.genere_liste_stat(qui) # Etape 3 bouton : ce bouton n'est actif que si admin a levé toutes les alertes. ### Testons s'il reste encore des alertes dans les fichiers admin # Récupération des fichiers admin list_fich = { Fichier(fich) for fich in glob.glob( os.path.join(os.curdir, "data", "admin_*.xml")) } alertes = False while not (alertes) and len( list_fich ) > 0: # à la première alerte détectée alertes = True fich = list_fich.pop() alertes = (True in { '- Alerte :' in Fichier.get(cand, 'Motifs') for cand in fich if Fichier.get(cand, 'Correction') != 'NC' }) ### Suite txt = '' if len(data['liste_admin'] ) > 0: # si les fichiers admin existent : txt = '<input type = "button" class ="fichier" value = "Générer les fichiers commission"' affich = '' if (alertes): affich = 'disabled' txt += 'onclick = "genere_wait();" {}/>'.format(affich) data['bout_etap3'] = txt # Envoyez le menu contenu = Composeur.html[patron].format(**data) # Composition de la page page += Composeur.html["MEP_MENU"].format(**{ 'contenu': contenu, 'script': qui.script_menu }) page += '</html>' return page
def traiter(self, **kwargs): """ Traiter un dossier """ # Fonction lancée par la fonction "traiter" du Serveur, elle même lancée par un clic sur 'Classé' ou 'NC' # On récupère le candidat courant cand = self.get_cand() # Ici, on va répercuter les complétions de l'administrateur dans tous les dossiers que le candidat a déposé. # Attention ! le traitement du fichier en cours est fait à part car deux objets 'Fichier' qui # auraient le même nom sont malgré tout différents !! On rajoute la bonne instance Fichier juste après. # Recherche de tous les fichiers existants (sauf fichier en cours) : list_fich_admin = [Fichier(fich) for fich in glob.glob(os.path.join(os.curdir, "data", "admin_*.xml"))\ if fich != self.fichier.nom] # On restreint la liste aux fichiers contenant le candidat en cours list_fich_cand = [fich for fich in list_fich_admin if cand in fich] # On rajoute le fichier suivi actuellement list_fich_cand.append(self.fichier) # list_fich_cand contient tous les fichiers dans lesquels le candidat courant se trouve. # ############### Admin a-t-il changé qqc ? Si oui, mise à jour. # Classe actuelle ? if Fichier.get(cand, 'Classe actuelle') != kwargs['Classe actuelle']: for fich in list_fich_cand: Fichier.set(fich.get_cand(cand), 'Classe actuelle', kwargs['Classe actuelle']) # Cas des notes matiere = ['Mathématiques', 'Physique/Chimie'] date = ['trimestre 1', 'trimestre 2', 'trimestre 3'] classe = ['Première', 'Terminale'] for cl in classe: for mat in matiere: for da in date: key = '{} {} {}'.format(mat, cl, da) if Fichier.get(cand, key) != kwargs[key]: # la note a-t-elle été modifiée ? for fich in list_fich_cand: Fichier.set(fich.get_cand(cand), key, kwargs[key]) # CPES et EAF #liste = ['Mathématiques CPES', 'Physique/Chimie CPES', 'Écrit EAF', 'Oral EAF'] # Seulement EAF depuis 2020 liste = ['Écrit EAF', 'Oral EAF'] for li in liste: if 'cpes' in li.lower(): if ('cpes' in Fichier.get(cand, 'Classe actuelle').lower()) and Fichier.get(cand, li) != kwargs[li]: for fich in list_fich_cand: Fichier.set(fich.get_cand(cand), li, kwargs[li]) else: if Fichier.get(cand, li) != kwargs[li]: for fich in list_fich_cand: Fichier.set(fich.get_cand(cand), li, kwargs[li]) # Commentaire éventuel admin + gestion des 'NC' # Les commentaires admin sont précédés de '- Admin :' c'est à cela qu'on les reconnaît. Et le jury les # verra sur fond rouge dans la liste de ses dossiers. # Par ailleurs, dossiers_jury.js exclut qu'un tel commentaire soit considéré comme une motivation de jury. motif = kwargs['motif'] if not('- Admin :' in motif or motif == '' or '- Alerte :' in motif): motif = '- Admin : {}'.format(motif) # Récupération de la correction. On en fait qqc seulement si elle est minimale (NC) cor = kwargs['correc'] # récupération de la correction et calcul du score final if float(cor) == float(min_correc): # L'admin a validé le formulaire avec la correction NC (le candidat ne passera pas en commission) # Pour ce cas là, on ne recopie pas dans toutes les filières. Admin peut exclure une candidature # dans une filière sans l'exclure des autres. Sécurité ! Fichier.set(cand, 'Correction', 'NC') # la fonction calcul_scoreb renverra 0 ! Fichier.set(cand, 'Jury', 'Admin') # Cette exclusion est un choix de l'admin (apparaît dans les tableaux) Fichier.set(cand, 'Motifs', motif) else: Fichier.set(cand, 'Correction', '0') # 2 lignes nécessaires si l'admin a NC un candidat, puis a changé d'avis. Fichier.set(cand, 'Jury', '') for fich in list_fich_cand: Fichier.set(fich.get_cand(cand), 'Motifs', motif) # On (re)calcule le score brut ! Fichier.calcul_scoreb(cand) # On sauvegarde tous les fichiers retouchés for fich in list_fich_cand: fich.sauvegarde()