diff --git a/.gitignore b/.gitignore index c6ef218..5c7c681 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ .idea - +.directory diff --git a/API_Interfaces.txt b/API_Interfaces.txt index 4760495..9a73149 100644 --- a/API_Interfaces.txt +++ b/API_Interfaces.txt @@ -20,20 +20,13 @@ GET -> Get the current logged user, return None if no one is connected Out: 200 -> USER = |null : Dictionary containing user infos or null - -######################## -Redirect to cas auth (/login) -######################## -Redirect to cas auth - - ######################## UserAPI (api/user) ######################## POST -> Create a user if it not already exists In: - CASid = Login of the user caught from the CAS authentication - role = Role of the user (can be concatenated with -) 1=secrétaire, 2=resp_formation, 3=tuteur_univ, 4=étudiant + email = Email and login of the user (must be unique) + role = Role of the user (can be concatenated with -) 1=secrétaire, 2=resp_formation, 3=tuteur_univ, 4=étudiant, 5=tuteur_ent Out: 200 -> UID = : The user already exists with the id USER_ID 201 -> UID = : The user has been successfully created with the id USER_ID @@ -41,17 +34,67 @@ POST -> Create a user if it not already exists PUT -> Modify an existing user In: (Suffix = /byuid/) - CASid = Login of the user caught from the CAS authentication - role = Role of the user (can be concatenated with -) 1=secrétaire, 2=resp_formation, 3=tuteur_univ, 4=étudiant + role = Role of the user (can be concatenated with -) 1=secrétaire, 2=resp_formation, 3=tuteur_univ, 4=étudiant, 5=tuteur_ent phone = Phone number of the user (00.00.00.00.00) - email = Email of the user + email = Email of the user (must be unique) Out: 200 -> UID = : The user has been modified sucessfully with the id USER_ID 400 -> ERROR = "One or more parameters are missing !" : Bad request 405 -> ERROR = "This user doesn't exists !" : Bad USER_ID provided + 405 -> ERROR = "A user with this email already exists !" : A user with this email already exists GET -> Getting specified user infos - In: (Suffixes = /byuid/ | /bylogin/ | /byemail/) + In: (Suffixes = /byuid/ | /byemail/) Out: 200 -> USER = |null : Dictionary containing user infos or null +######################## +GroupAPI (api/group) +######################## +POST -> Create a group if it not already exists + In: + name = Name of the group (must be unique) + year = Parameter setting the year + class_short = Parameter setting the short name of the class + class_long = Parameter setting the full name of the class + department = Parameter setting the name of the class's department + resp_id = UID of the group's responsible + sec_id = UID of the group's secretary + Out: + 200 -> GID = : The group already exists with the id GROUP_ID + 201 -> GID = : The group has been successfully created with the id GROUP_ID + 400 -> ERROR = "One or more parameters are missing" : Bad request + 400 -> ERROR = "The user with id doesn't exists !" : The given USER_ID for resp_id or sec_id is not found + +PUT -> Modify an existing group + In: (Suffix = /bygid/) + name = Name of the group (must be unique) + year = Parameter setting the year + class_short = Parameter setting the short name of the class + class_long = Parameter setting the full name of the class + department = Parameter setting the name of the class's department + resp_id = UID of the group's responsible + sec_id = UID of the group's secretary + Out: + 200 -> GID = : The group has been modified sucessfully with the id GROUP_ID + 400 -> ERROR = "One or more parameters are missing !" : Bad request + 400 -> ERROR = "The user with id doesn't exists !" : The given USER_ID for resp_id or sec_id is not found + 405 -> ERROR = "This group doesn't exists !" : Bad GROUP_ID provided + 405 -> ERROR = "A group with this name already exists !" : A group with this name already exists + +GET -> Getting specified group infos + In: (Suffixes = /bygid/ | /byname/ ) + Out: + 200 -> GROUP = |null : Dictionary containing group infos or null + +OPTIONS -> Add pairs of users (student/tutor) to the group + In: + pairs -> Table of pairs student's uid/tutor's uid (ex: [[1,2],[3,2]]) + Out: + 200 -> RESULT = "Pairs added successfully" + 400 -> ERROR = "One or more parameters are missing !" : Bad request + 400 -> ERROR = "The user with id doesn't exists !" : The given USER_ID for student or tutor is not found + 400 -> ERROR = "A student must have the 'student' role !" : The given USER_ID for student doesn't have the "student" role (4) + 400 -> ERROR = "A student can't be a tutor !" : The given USER_ID for tutor have the "student" role (4) and so can't be a tutor + 405 -> ERROR = "This group doesn't exists !" : Bad GROUP_ID provided + 409 -> ERROR = "Pairs are incorrectly formed !" : Bad syntax in pairs table diff --git a/Template_PDF/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom.pdf new file mode 100644 index 0000000..18226d5 Binary files /dev/null and b/Template_PDF/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom.pdf differ diff --git a/Template_PDF/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom.pdf new file mode 100644 index 0000000..0c9b3f0 Binary files /dev/null and b/Template_PDF/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom.pdf differ diff --git a/Template_PDF/LivretAlternant_Accueil_Page3_Livret_Engagements_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_Accueil_Page3_Livret_Engagements_NOM_Prenom.pdf new file mode 100644 index 0000000..3ea3072 Binary files /dev/null and b/Template_PDF/LivretAlternant_Accueil_Page3_Livret_Engagements_NOM_Prenom.pdf differ diff --git a/Template_PDF/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom.pdf new file mode 100644 index 0000000..c236e5f Binary files /dev/null and b/Template_PDF/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom.pdf differ diff --git a/Template_PDF/LivretAlternant_Accueil_Page5_Bis_Periodes_ReglesAbsences.pdf b/Template_PDF/LivretAlternant_Accueil_Page5_Bis_Periodes_ReglesAbsences.pdf new file mode 100644 index 0000000..78a1747 Binary files /dev/null and b/Template_PDF/LivretAlternant_Accueil_Page5_Bis_Periodes_ReglesAbsences.pdf differ diff --git a/Template_PDF/LivretAlternant_P12345_ReleveAbsencesNonJustifi‚es_NOM_Prenom_.pdf b/Template_PDF/LivretAlternant_P12345_ReleveAbsencesNonJustifi‚es_NOM_Prenom_.pdf new file mode 100644 index 0000000..51cf2d2 Binary files /dev/null and b/Template_PDF/LivretAlternant_P12345_ReleveAbsencesNonJustifi‚es_NOM_Prenom_.pdf differ diff --git a/Template_PDF/LivretAlternant_PEntreprise_Bilan_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_PEntreprise_Bilan_NOM_Prenom.pdf new file mode 100644 index 0000000..091741b Binary files /dev/null and b/Template_PDF/LivretAlternant_PEntreprise_Bilan_NOM_Prenom.pdf differ diff --git a/Template_PDF/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom.pdf b/Template_PDF/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom.pdf new file mode 100644 index 0000000..7bc03ce Binary files /dev/null and b/Template_PDF/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom.pdf differ diff --git a/Template_PDF/backup/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom_backup.pdf b/Template_PDF/backup/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom_backup.pdf new file mode 100644 index 0000000..2aec054 Binary files /dev/null and b/Template_PDF/backup/LivretAlternant_Accueil_Page1_Couverture_NOM_Prenom_backup.pdf differ diff --git a/Template_PDF/backup/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom_backup.pdf b/Template_PDF/backup/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom_backup.pdf new file mode 100644 index 0000000..84e6d6e Binary files /dev/null and b/Template_PDF/backup/LivretAlternant_Accueil_Page2_Sommaire_FicheRenseignements_NOM_Prenom_backup.pdf differ diff --git a/Template_PDF/backup/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom_backup.pdf b/Template_PDF/backup/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom_backup.pdf new file mode 100644 index 0000000..c291fa3 Binary files /dev/null and b/Template_PDF/backup/LivretAlternant_Accueil_Page4_PosteOccupe_NOM_Prenom_backup.pdf differ diff --git a/Template_PDF/backup/LivretAlternant_PEntreprise_Bilan_NOM_Prenom_backup.pdf b/Template_PDF/backup/LivretAlternant_PEntreprise_Bilan_NOM_Prenom_backup.pdf new file mode 100644 index 0000000..7cc7989 Binary files /dev/null and b/Template_PDF/backup/LivretAlternant_PEntreprise_Bilan_NOM_Prenom_backup.pdf differ diff --git a/Template_PDF/backup/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom_backup.pdf b/Template_PDF/backup/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom_backup.pdf new file mode 100644 index 0000000..a9b242a Binary files /dev/null and b/Template_PDF/backup/LivretAlternant_PFormation_Bis_Bilan_NOM_Prenom_backup.pdf differ diff --git a/Template_PDF/specification_template.txt b/Template_PDF/specification_template.txt new file mode 100644 index 0000000..4b8d9fa --- /dev/null +++ b/Template_PDF/specification_template.txt @@ -0,0 +1,67 @@ +Page 1 : + +MASTER : {{ nom_master }} + +{{ nom_complet_master }} + +Année universitaire : + +{{ annee_1 }} +- +{{ annee_2 }} + +De : {{ nom_prenom }} + +mail : {{ email }} + +tel : {{ telephone }} + +Alternant à : {{ entreprise }} + +tuteur/tutrice pédagogique : {{ tuteur_pedagogique }} + +Tuteur / Tutrice entreprise : {{ tuteur_entreprise }} + + +Page 2 : +Alternant +Type de contrat : +{{ type_contrat_apprentissage | X }} +{{ type_contrat_professionnalisation | X }} +{{ type_contrat_stage | X }} + +Début de contrat : {{ debut_contrat }} +Fin de contrat : {{ fin_contrat }} +tel : {{ telephone }} +mail : {{ email }} +compostant de formation : {{ compostant_formation }} +responsable tuteur_pedagogique de la formation : {{ responsable_pedagogique_formation }} +tel : {{ tel_responsable_pedagogique_formation }} +mail : {{ mail_responsable_pedagogique_formation }} + +tuteur pédagogique : +{{ tel_tuteur_pedagogique }} +{{ mail_tuteur_pedagogique }} + +tuteur entreprise : +{{ tuteur_entreprise }} +entreprise : {{ entreprise }} +adresse lieu Alternance : {{ adresse_entreprise }} +tel : {{ tel_tuteur_entreprise }} +mail : {{ mail_tuteur_entreprise }} + +Page 4 : +poste occupé : {{ poste_occupe }} +{{ poste_occupe_2 }} + +Pentreprise : +{{ n_periode }} +{{ debut_periode }} +{{ fin_periode }} +{{ travaux_entreprise }} +{{ remarque_tuteur }} + +bilan_periode : + +{{ n_periode }} +{{ bilan_periode }} diff --git a/backend/.gitignore b/backend/.gitignore index e976e61..415cdd1 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -31,3 +31,4 @@ coverage.xml #Config files config.py +/app/OLA_RESSOURCES/ diff --git a/backend/OLA.mysql b/backend/OLA.mysql index 7958fb2..79529a8 100644 --- a/backend/OLA.mysql +++ b/backend/OLA.mysql @@ -37,11 +37,13 @@ CREATE TABLE IF NOT EXISTS `GROUP` CREATE TABLE IF NOT EXISTS `USER` ( - id BIGINT NOT NULL AUTO_INCREMENT, - `login` VARCHAR(128) NOT NULL, - `role` VARCHAR(10) NOT NULL, - email VARCHAR(256) NOT NULL, - phone VARCHAR(15), + id BIGINT NOT NULL AUTO_INCREMENT, + `role` VARCHAR(10) NOT NULL, + email VARCHAR(128) NOT NULL, + name VARCHAR(128) NOT NULL, + psw VARCHAR(256) DEFAULT NULL, + hash VARCHAR(128), + phone VARCHAR(15), PRIMARY KEY(id) ) ENGINE = INNODB; @@ -56,19 +58,17 @@ CREATE TABLE IF NOT EXISTS TUTORSHIP CREATE TABLE IF NOT EXISTS LIVRET ( - id BIGINT NOT NULL AUTO_INCREMENT, - tutorship_id BIGINT, - etutor_name VARCHAR(128) NOT NULL, - etutor_email VARCHAR(256) NOT NULL, - etutor_phone VARCHAR(15) NOT NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + tutorship_id BIGINT NOT NULL, + etutor_id BIGINT NOT NULL, company_name VARCHAR(256) NOT NULL, company_address VARCHAR(512) NOT NULL, - contract_type INT NOT NULL, - contract_start DATE NOT NULL, - contract_end DATE NOT NULL, + contract_type INT NOT NULL, + contract_start DATE NOT NULL, + contract_end DATE NOT NULL, ressources_dir VARCHAR(512), - opened TINYINT(1) NOT NULL, - expire DATE NOT NULL, + opened TINYINT(1) NOT NULL, + expire DATE NOT NULL, PRIMARY KEY(id) ) ENGINE = INNODB; @@ -85,14 +85,6 @@ CREATE TABLE IF NOT EXISTS PERIOD PRIMARY KEY (id) ) ENGINE = INNODB; -CREATE TABLE IF NOT EXISTS HASHTABLE -( - token VARCHAR(255) NOT NULL, - exipre DATE NOT NULL, - period_id BIGINT NOT NULL, - PRIMARY KEY(token) -) ENGINE = INNODB; - # Create FKs ALTER TABLE `GROUP` @@ -127,6 +119,10 @@ REFERENCES TUTORSHIP (id) ON DELETE CASCADE ON UPDATE CASCADE ; + +ALTER TABLE LIVRET + ADD FOREIGN KEY (etutor_id) +REFERENCES `USER` (id); ALTER TABLE LIVRET ADD FOREIGN KEY (tutorship_id) @@ -140,14 +136,10 @@ ALTER TABLE PERIOD REFERENCES LIVRET (id) ON DELETE CASCADE ON UPDATE CASCADE; - -ALTER TABLE HASHTABLE - ADD FOREIGN KEY (period_id) -REFERENCES PERIOD (id) - ON DELETE CASCADE - ON UPDATE CASCADE -; + # Create Indexes -CREATE INDEX tutor_email ON LIVRET(etutor_email); -CREATE INDEX user_login ON `USER`(`login`); +CREATE UNIQUE INDEX user_email + ON `USER` (`email`); +CREATE UNIQUE INDEX user_hash + ON `USER` (`hash`); diff --git a/backend/OLA_DATA.mysql b/backend/OLA_DATA.mysql new file mode 100644 index 0000000..7c8db3a --- /dev/null +++ b/backend/OLA_DATA.mysql @@ -0,0 +1,25 @@ +USE OLA; +INSERT INTO SETTINGS VALUES ('BASE_DIRECTORY', '/OLA_RESSOURCES/', 'Répertoire base pour le dépot des fichiers'); +INSERT INTO SETTINGS VALUES ('TEMPLATES_DIRECTORY', '/OLA_TEMPLATES/', 'Répertoire base pour le dépot des fichiers'); +INSERT INTO SETTINGS VALUES ('OLA_URL', 'ola.univ-tlse2.fr/', 'URL de l application'); + +INSERT INTO `USER` VALUES (1, '1', 'sec@univ-tlse2.fr', 'Secrétaire', DEFAULT, 'aZeRtYuIoP', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (2, '4', 'etu1@univ-tlse2.fr', 'Etudiant 1', DEFAULT, 'qSdFgHjKlM', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (3, '4', 'etu2@univ-tlse2.fr', 'Etudiant 2', DEFAULT, 'wXcVbN', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (4, '4', 'etu3@univ-tlse2.fr', 'Etudiant 3', DEFAULT, 'pOiUyTrEzA', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (5, '2-3', 'resp@univ-tlse2.fr', 'Responsable', DEFAULT, 'mLkJhGfDsQ', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (6, '3', 'tut@univ-tlse2.fr', 'Tuteur Pédagogique', DEFAULT, 'nBvCxW', '01.23.45.67.89'); + +INSERT INTO `GROUP` VALUES + (1, 'M2_ICE_2016-2017_TEST', '2017', 'Master2 ICE', 'Master 2 Informatique Collaborative en Entreprise', + 'Sciences du chômage proffessionnel', 5, 1, + '/home/dan/PycharmProjects/OLA/backend/app/OLA_RESSOURCES/M2_ICE_2016-2017_TEST'); +INSERT INTO `GROUP` VALUES + (2, 'M1_ICE_2016-2017_TEST', '2017', 'Master1 ICE', 'Master 1 Informatique Collaborative en Entreprise', + 'Sciences du chômage proffessionnel', 5, 1, + '/home/dan/PycharmProjects/OLA/backend/app/OLA_RESSOURCES/M1_ICE_2016-2017_TEST'); + +INSERT INTO TUTORSHIP VALUES (DEFAULT, 1, 5, 2); +INSERT INTO TUTORSHIP VALUES (DEFAULT, 2, 5, 4); +INSERT INTO TUTORSHIP VALUES (DEFAULT, 1, 6, 3); + diff --git a/backend/app/OLA_DATA.mysql b/backend/app/OLA_DATA.mysql new file mode 100644 index 0000000..1daf4d5 --- /dev/null +++ b/backend/app/OLA_DATA.mysql @@ -0,0 +1,18 @@ +USE OLA; +INSERT INTO SETTINGS VALUES ('URL_BASE_DIRECTORY', '/OLA_RESSOURCES/', 'Répertoire base pour le dépot des fichiers'); +INSERT INTO SETTINGS VALUES ('OLA_URL', 'ola.univ-tlse2.fr/', 'URL de l application'); + +INSERT INTO `USER` VALUES (1, 'sec', '1', 'sec@univ-tlse2.fr', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (2, 'etu1', '4', 'etu1@univ-tlse2.fr', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (3, 'etu2', '4', 'etu2@univ-tlse2.fr', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (4, 'etu3', '4', 'etu3@univ-tlse2.fr', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (5, 'resp', '2-3', 'resp@univ-tlse2.fr', '01.23.45.67.89'); +INSERT INTO `USER` VALUES (6, 'tut', '3', 'tut@univ-tlse2.fr', '01.23.45.67.89'); + +INSERT INTO `GROUP` VALUES (1, 'M2_ICE_2016-2017_TEST', '2017', 'Master2 ICE', 'Master 2 Informatique Collaborative en Entreprise', 'Sciences du chômage proffessionnel', 5, 1, '/home/dan/PycharmProjects/OLA/backend/app/OLA_RESSOURCES/M2_ICE_2016-2017_TEST'); +INSERT INTO `GROUP` VALUES (2, 'M1_ICE_2016-2017_TEST', '2017', 'Master1 ICE', 'Master 1 Informatique Collaborative en Entreprise', 'Sciences du chômage proffessionnel', 5, 1, '/home/dan/PycharmProjects/OLA/backend/app/OLA_RESSOURCES/M1_ICE_2016-2017_TEST'); + +INSERT INTO TUTORSHIP VALUES (DEFAULT, 1, 2, 5); +INSERT INTO TUTORSHIP VALUES (DEFAULT, 2, 4, 5); +INSERT INTO TUTORSHIP VALUES (DEFAULT, 1, 3, 6); + diff --git a/backend/app/api/GroupAPI.py b/backend/app/api/GroupAPI.py new file mode 100644 index 0000000..a8b13c3 --- /dev/null +++ b/backend/app/api/GroupAPI.py @@ -0,0 +1,221 @@ +import os + +from flask_restful import Resource, request + +from app.api import mailsModels +from app.model import * +from app.utils import * + + +class GroupAPI(Resource): + """ + Group Api Resource + """ + + def post(self): + args = request.get_json(cache=False, force=True) + if not checkParams(['name', 'year', 'class_short', 'class_long', 'department', 'resp_id', 'sec_id'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + name = args['name'] + year = args['year'] + class_short = args['class_short'] + class_long = args['class_long'] + department = args['department'] + resp_id = args['resp_id'] + sec_id = args['sec_id'] + res_dir = getParam('BASE_DIRECTORY') + name + "/" + mails = [] + + group = getGroup(name=name) + if group is not None: + return {"GID": group["id"]}, 200 + + user = getUser(uid=resp_id) + if user is None: + return {"ERROR": "The user with id " + str(resp_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_RESP_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("RESP_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "2" not in user['role'].split('-'): + role = user['role'] + "-2" + query = USER.update().values(role=role).where(USER.c.id == resp_id) + query.execute() + + user = getUser(uid=sec_id) + if user is None: + return {"ERROR": "The user with id " + str(sec_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_SEC_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("SEC_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "1" not in user['role'].split('-'): + role = user['role'] + "-1" + query = USER.update().values(role=role).where(USER.c.id == sec_id) + query.execute() + + query = GROUP.insert().values(name=name, year=year, class_short=class_short, class_long=class_long, + department=department, resp_id=resp_id, sec_id=sec_id, ressources_dir=res_dir) + res = query.execute() + os.mkdir(res_dir) + + for m in mails: + addr = m[0] + mail = m[1] + send_mail(mail[0], addr, mail[1]) + + return {"GID": res.lastrowid}, 201 + + def put(self, gid): + args = request.get_json(cache=False, force=True) + if not checkParams(['name', 'year', 'class_short', 'class_long', 'department', 'resp_id', 'sec_id'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + name = args['name'].replace(" ", "_").replace("/", "-") + year = args['year'] + class_short = args['class_short'] + class_long = args['class_long'] + department = args['department'] + resp_id = args['resp_id'] + sec_id = args['sec_id'] + res_dir = getParam('BASE_DIRECTORY') + name + "/" + mails = [] + + group = getGroup(gid=gid) + if group is None: + return {"ERROR": "This group does not exists !"}, 405 + + group2 = getGroup(name=name) + if group2 is not None: + return {"ERROR": "A group with this name already exists !"}, 405 + + user = getUser(uid=resp_id) + if user is None: + return {"ERROR": "The user with id " + str(resp_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_RESP_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("RESP_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "2" not in user['role'].split('-'): + role = user['role'] + "-2" + query = USER.update().values(role=role).where(USER.c.id == resp_id) + query.execute() + + user = getUser(uid=sec_id) + if user is None: + return {"ERROR": "The user with id " + str(sec_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_SEC_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("SEC_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "1" not in user['role'].split('-'): + role = user['role'] + "-1" + query = USER.update().values(role=role).where(USER.c.id == sec_id) + query.execute() + + query = GROUP.update().values(name=name, year=year, class_short=class_short, class_long=class_long, + department=department, resp_id=resp_id, sec_id=sec_id, ressources_dir=res_dir) \ + .where(GROUP.c.id == gid) + query.execute() + + if group["ressources_dir"] != res_dir: + os.rename(group["ressources_dir"], res_dir) + + for m in mails: + addr = m[0] + mail = m[1] + send_mail(mail[0], addr, mail[1]) + + return {"GID": gid}, 200 + + def get(self, gid=0, name=""): + if gid > 0: + return {'GROUP': getGroup(gid=gid)}, 200 + elif name != "": + return {'GROUP': getGroup(name=name)}, 200 + + def options(self, gid): + args = request.get_json(cache=False, force=True) + if not checkParams(['pairs'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + pairs = args["pairs"] + + group = getGroup(gid=gid) + if group is None: + return {"ERROR": "This group does not exists !"}, 405 + + for p in pairs: + try: + stud = getUser(uid=p[0]) + if stud is None: + return {"ERROR": "The user with id " + str(p[0]) + " does not exists !"}, 400 + elif stud['role'] != "4": + return {"ERROR": "A student must have the 'student' role !"}, 400 + + tutor = getUser(uid=p[1]) + if tutor is None: + return {"ERROR": "The user with id " + str(p[1]) + " does not exists !"}, 400 + elif tutor['role'] == "4": + return {"ERROR": "A student can't be a tutor !"}, 400 + elif "3" not in tutor['role'].split('-'): + role = tutor['role'] + "-3" + query = USER.update().values(role=role).where(USER.c.id == p[1]) + query.execute() + except IndexError: + return {"ERROR": "Pairs are incorrectly formed !"}, 409 + + query = TUTORSHIP.insert().values(group_id=gid, student_id=p[0], ptutor_id=p[1]) + query.execute() + + query = USER.select(USER.c.id == stud["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_STUD_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("STUD_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + send_mail(mail[0], stud["email"], mail[1]) + + return {"RESULT": "Pairs added successfully"}, 200 diff --git a/backend/app/api/LivretAPI.py b/backend/app/api/LivretAPI.py new file mode 100644 index 0000000..ecd87cd --- /dev/null +++ b/backend/app/api/LivretAPI.py @@ -0,0 +1,221 @@ +import os + +from flask_restful import Resource, request + +from app.api import mailsModels +from app.model import * +from app.utils import * + + +class LivretAPI(Resource): + """ + Livret Api Resource + """ + + def post(self): + args = request.get_json(cache=False, force=True) + if not checkParams(['name', 'year', 'class_short', 'class_long', 'department', 'resp_id', 'sec_id'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + name = args['name'] + year = args['year'] + class_short = args['class_short'] + class_long = args['class_long'] + department = args['department'] + resp_id = args['resp_id'] + sec_id = args['sec_id'] + res_dir = getParam('BASE_DIRECTORY') + name + "/" + mails = [] + + group = getGroup(name=name) + if group is not None: + return {"GID": group["id"]}, 200 + + user = getUser(uid=resp_id) + if user is None: + return {"ERROR": "The user with id " + str(resp_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_RESP_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("RESP_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "2" not in user['role'].split('-'): + role = user['role'] + "-2" + query = USER.update().values(role=role).where(USER.c.id == resp_id) + query.execute() + + user = getUser(uid=sec_id) + if user is None: + return {"ERROR": "The user with id " + str(sec_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_SEC_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("SEC_OF_GROUP", {"GROUP": name, + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "1" not in user['role'].split('-'): + role = user['role'] + "-1" + query = USER.update().values(role=role).where(USER.c.id == sec_id) + query.execute() + + query = GROUP.insert().values(name=name, year=year, class_short=class_short, class_long=class_long, + department=department, resp_id=resp_id, sec_id=sec_id, ressources_dir=res_dir) + res = query.execute() + os.mkdir(res_dir) + + for m in mails: + addr = m[0] + mail = m[1] + send_mail(mail[0], addr, mail[1]) + + return {"GID": res.lastrowid}, 201 + + def put(self, gid): + args = request.get_json(cache=False, force=True) + if not checkParams(['name', 'year', 'class_short', 'class_long', 'department', 'resp_id', 'sec_id'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + name = args['name'].replace(" ", "_").replace("/", "-") + year = args['year'] + class_short = args['class_short'] + class_long = args['class_long'] + department = args['department'] + resp_id = args['resp_id'] + sec_id = args['sec_id'] + res_dir = getParam('BASE_DIRECTORY') + name + "/" + mails = [] + + group = getGroup(gid=gid) + if group is None: + return {"ERROR": "This group does not exists !"}, 405 + + group2 = getGroup(name=name) + if group2 is not None: + return {"ERROR": "A group with this name already exists !"}, 405 + + user = getUser(uid=resp_id) + if user is None: + return {"ERROR": "The user with id " + str(resp_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_RESP_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("RESP_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "2" not in user['role'].split('-'): + role = user['role'] + "-2" + query = USER.update().values(role=role).where(USER.c.id == resp_id) + query.execute() + + user = getUser(uid=sec_id) + if user is None: + return {"ERROR": "The user with id " + str(sec_id) + " does not exists !"}, 400 + else: + query = USER.select(USER.c.id == user["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_SEC_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("SEC_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + mails.append((user["email"], mail)) + if "1" not in user['role'].split('-'): + role = user['role'] + "-1" + query = USER.update().values(role=role).where(USER.c.id == sec_id) + query.execute() + + query = GROUP.update().values(name=name, year=year, class_short=class_short, class_long=class_long, + department=department, resp_id=resp_id, sec_id=sec_id, ressources_dir=res_dir) \ + .where(GROUP.c.id == gid) + query.execute() + + if group["ressources_dir"] != res_dir: + os.rename(group["ressources_dir"], res_dir) + + for m in mails: + addr = m[0] + mail = m[1] + send_mail(mail[0], addr, mail[1]) + + return {"GID": gid}, 200 + + def get(self, gid=0, name=""): + if gid > 0: + return {'GROUP': getGroup(gid=gid)}, 200 + elif name != "": + return {'GROUP': getGroup(name=name)}, 200 + + def options(self, gid): + args = request.get_json(cache=False, force=True) + if not checkParams(['pairs'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + + pairs = args["pairs"] + + group = getGroup(gid=gid) + if group is None: + return {"ERROR": "This group does not exists !"}, 405 + + for p in pairs: + try: + stud = getUser(uid=p[0]) + if stud is None: + return {"ERROR": "The user with id " + str(p[0]) + " does not exists !"}, 400 + elif stud['role'] != "4": + return {"ERROR": "A student must have the 'student' role !"}, 400 + + tutor = getUser(uid=p[1]) + if tutor is None: + return {"ERROR": "The user with id " + str(p[1]) + " does not exists !"}, 400 + elif tutor['role'] == "4": + return {"ERROR": "A student can't be a tutor !"}, 400 + elif "3" not in tutor['role'].split('-'): + role = tutor['role'] + "-3" + query = USER.update().values(role=role).where(USER.c.id == p[1]) + query.execute() + except IndexError: + return {"ERROR": "Pairs are incorrectly formed !"}, 409 + + query = TUTORSHIP.insert().values(group_id=gid, student_id=p[0], ptutor_id=p[1]) + query.execute() + + query = USER.select(USER.c.id == stud["id"]) + rows = query.execute() + res = rows.first() + if res.hash is not None and len(res.hash) > 0: + mail = mailsModels.getMailContent("NEW_STUD_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL') + "registration/" + + res.hash}) + else: + mail = mailsModels.getMailContent("STUD_OF_GROUP", {"GROUP": group["name"], + "URL": getParam('OLA_URL')}) + + send_mail(mail[0], stud["email"], mail[1]) + + return {"RESULT": "Pairs added successfully"}, 200 diff --git a/backend/app/api/LoginAPI.py b/backend/app/api/LoginAPI.py new file mode 100644 index 0000000..c426fb3 --- /dev/null +++ b/backend/app/api/LoginAPI.py @@ -0,0 +1,51 @@ +from hashlib import sha256 + +from flask import session, request +from flask_restful import Resource + +from app.core import app +from app.model import USER, getUser +from app.utils import checkParams + + +class LoginAPI(Resource): + """ + Login Api Resource + """ + + def post(self): + args = request.get_json(cache=False, force=True) + if not checkParams(['email', 'password'], args): + return {"ERROR": "One or more parameters are missing !"}, 400 + email = args['email'] + psw = args['password'] + password = sha256(psw.encode('utf-8')).hexdigest() + + if "user" in session and session["user"] is not None: + return {'AUTH_RESULT': 'ALREADY_LOGGED'}, 201 + + query = USER.select(USER.c.email == email) + rows = query.execute() + res = rows.first() + + if app.config['TESTING']: + if res is not None and psw == email: + user = getUser(uid=res.id) + session['user'] = user + return {'AUTH_RESULT': 'OK'}, 200 + else: + session['user'] = None + return {'AUTH_RESULT': 'AUTHENTICATION_FAILED'}, 401 + else: + if res is not None and password != "" and password == res.psw: + user = getUser(uid=res.id) + session['user'] = user + return {'AUTH_RESULT': 'OK'}, 200 + else: + session['user'] = None + return {'AUTH_RESULT': 'AUTHENTICATION_FAILED'}, 401 + + def delete(self): + session['user'] = None + return {'AUTH_RESULT': 'OK'}, 200 + diff --git a/backend/app/api/PdfAPI.py b/backend/app/api/PdfAPI.py new file mode 100644 index 0000000..a1e7105 --- /dev/null +++ b/backend/app/api/PdfAPI.py @@ -0,0 +1,43 @@ +import os + +from flask import request +from flask_restful import Resource +from flask_restful.reqparse import RequestParser +from model import getParam + +from app.model import getGroup +from app.tools.LibPdf import delete_file, upload_file, allowed_file + + +class PdfAPI(Resource): + """ + Pdf Api Resource + """ + + def delete(self): + parser = RequestParser() + parser.add_argument('templateName', required=True, help="Template name is required !") + args = parser.parse_args() + + if ".." in args: + return {"msg": ".. not allowed in path"}, 400 + + delete_file(os.path.join(getParam('TEMPLATES_DIRECTORY'), args['templateName'])) + + def post(self): + """ + Upload d'un template + :return: + """ + parser = RequestParser() + parser.add_argument('groupeName', required=True, help="id/name groupe cannot be blank!") + args = parser.parse_args() + + group = getGroup(args['groupe']) + file = request.files['file'] + + if file.filename == '': + return {"message": "Fichier non trouve"}, 400 + + if file and allowed_file(file.filename): + upload_file(file, group["ressources_dir"]) diff --git a/backend/app/api/UserAPI.py b/backend/app/api/UserAPI.py index 2560302..f9bfbcb 100644 --- a/backend/app/api/UserAPI.py +++ b/backend/app/api/UserAPI.py @@ -1,7 +1,9 @@ +from hashlib import sha256 + from flask_restful import Resource, request from app.model import * -from app.utils import checkParams +from app.utils import checkParams, get_random_string class UserAPI(Resource): @@ -11,45 +13,60 @@ class UserAPI(Resource): def post(self): args = request.get_json(cache=False, force=True) - if not checkParams(['CASid', 'role'], args): + if not checkParams(['role', 'email', 'name'], args): return {"ERROR": "One or more parameters are missing !"}, 400 - CASid = args['CASid'] role = args['role'] - email = self.getEmailFromCAS(CASid) + email = args['email'] + name = args['name'] phone = None - user = getUser(login=CASid) + user = getUser(email=email) + hashpass = get_random_string() + while hashExists(hashpass): + hashpass = get_random_string() + if user is not None: return {"UID": user["id"]}, 200 - query = USER.insert().values(login=CASid, email=email, role=role, phone=phone) + query = USER.insert().values(email=email, role=role, phone=phone, name=name, hash=hashpass) res = query.execute() return {"UID": res.lastrowid}, 201 def put(self, uid): args = request.get_json(cache=False, force=True) - if not checkParams(['CASid', 'role', 'email', 'phone'], args): + if not checkParams(['role', 'email', 'phone', 'name', 'password', 'firstname'], args): return {"ERROR": "One or more parameters are missing !"}, 400 + role = args['role'] + email = args['email'] + phone = args['phone'] + firstname = args['firstname'] + name = args['name'] + psw = args['password'] + + name = firstname.title() + " " + name.upper() + # TODO : Lors de l'ajout des fiches d'absence ca sera ça le critère de recherche + le groupe + + if psw is None or len(psw) < 8: + return {"ERROR": "Password can't be empty or less than 8 characters !"}, 400 + + password = sha256(psw.encode('utf-8')).hexdigest() + if getUser(uid=uid) is None: return {"ERROR": "This user doesn't exists !"}, 405 - CASid = args['CASid'] - role = args['role'] - email = args['email'] - phone = args['phone'] - query = USER.update().values(login=CASid, email=email, role=role, phone=phone).where(USER.c.id == uid) + if getUser(email=email) is not None: + return {"ERROR": "A user with this email already exists !"}, 405 + + query = USER.update().values(email=email, role=role, phone=phone, name=name, psw=password, hash=None) \ + .where(USER.c.id == uid) query.execute() return {"UID": uid}, 200 - def get(self, uid=0, login="", email=""): + def get(self, uid=0, email="", hashcode=""): if uid > 0: return {'USER': getUser(uid=uid)}, 200 - elif login != "": - return {'USER': getUser(login=login)}, 200 elif email != "": return {'USER': getUser(email=email)}, 200 - - @staticmethod - def getEmailFromCAS(CASid): - return "" + elif hashcode != "": + return {'USER': getUser(hashcode=hashcode)}, 200 diff --git a/backend/app/api/UserInfoAPI.py b/backend/app/api/UserInfoAPI.py index 55f3625..c1807e9 100644 --- a/backend/app/api/UserInfoAPI.py +++ b/backend/app/api/UserInfoAPI.py @@ -1,6 +1,8 @@ from flask import session from flask_restful import Resource +from app.model import * + class UserInfoAPI(Resource): """ @@ -8,5 +10,24 @@ class UserInfoAPI(Resource): """ def get(self): - user = session["user"] + user = session.get("user", None) return {'USER': user}, 200 + + +class UserGroupsAPI(Resource): + """ + UserGroups Api Resource + """ + + def get(self): + user = session.get("user", None) + if user is not None: + subquery = LIVRET.select().distinct().with_only_columns([LIVRET.c.tutorship_id]) + query = TUTORSHIP.select( + and_(TUTORSHIP.c.student_id == user["id"], TUTORSHIP.c.id.notin_(subquery))).distinct() + res = query.execute() + liste = [] + for r in res: + liste.append(r.group_id) + + return {'GROUP_LIST': liste}, 200 diff --git a/backend/app/api/loginAPI.py b/backend/app/api/loginAPI.py deleted file mode 100644 index cb7828d..0000000 --- a/backend/app/api/loginAPI.py +++ /dev/null @@ -1,38 +0,0 @@ -from flask import session -from flask_restful import Resource -from flask_restful.reqparse import RequestParser - -from app.model import * -from app.core import cas - -class LoginAPI(Resource): - """ - Login Api Resource - """ - - def get(self): - if "user" in session and session["user"] is not None: - return {'AUTH_RESULT': 'ALREADY_LOGGED'}, 201 - userInfo = self.getUserInfoFromCAS() - - if userInfo is not None: - user = getUser(login=userInfo['login']) - if user is not None and isUserAllowed(user["id"]): - session['user'] = user - return {'AUTH_RESULT': 'OK'}, 200 - else: - session['user'] = None - return {'AUTH_RESULT': 'NOT_ALLOWED'}, 403 - else: - session['user'] = None - return {'AUTH_RESULT': 'AUTHENTICATION_FAILED'}, 401 - - def delete(self): - session['user'] = None - return {'AUTH_RESULT': 'OK'}, 200 - - def getUserInfoFromCAS(self): - if cas.username is not None: - return {"login": cas.username} - else: - return None diff --git a/backend/app/api/mailsModels.py b/backend/app/api/mailsModels.py new file mode 100644 index 0000000..e928d94 --- /dev/null +++ b/backend/app/api/mailsModels.py @@ -0,0 +1,53 @@ +_NEW_STUD_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,

Votre compte vient d'être créé dans l'Outil du " + "Livret de l'Alternant dans le groupe #GROUPE. Vous pouvez dès " + "maintenant l'activer, puis créer un livret en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + +_STUD_OF_GROUP = ( + "Vous avez été ajouté à un groupe OLA !", "Bonjour,

Votre compte vient d'être ajouté dans l'Outil du " + "Livret de l'Alternant au groupe #GROUPE. Vous pouvez dès " + "maintenant créer un livret en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + +_NEW_RESP_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,

Votre compte vient d'être créé dans l'Outil du " + "Livret de l'Alternant en tant que responsable du groupe #GROUPE. Vous pouvez dès " + "maintenant l'activer, en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + +_RESP_OF_GROUP = ( + "Vous avez été ajouté à un groupe OLA !", "Bonjour,

Votre compte vient d'être ajouté dans l'Outil du " + "Livret de l'Alternant en tant que responsable du groupe #GROUPE. Vous pouvez dès " + "maintenant y accéder en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + +_NEW_SEC_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,

Votre compte vient d'être créé dans l'Outil du " + "Livret de l'Alternant en tant que secrétaire du groupe #GROUPE. Vous pouvez dès " + "maintenant l'activer, en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + +_SEC_OF_GROUP = ( + "Vous avez été ajouté à un groupe OLA !", "Bonjour,

Votre compte vient d'être ajouté dans l'Outil du " + "Livret de l'Alternant en tant que secrétaire du groupe #GROUPE. Vous pouvez dès " + "maintenant y accéder en vous rendant à l'adresse :
" + "#URL

Bonne journée !

") + + +def getMailContent(mail_type, args): + if mail_type == "NEW_STUD_OF_GROUP": + mail = _NEW_STUD_OF_GROUP + elif mail_type == "STUD_OF_GROUP": + mail = _STUD_OF_GROUP + elif mail_type == "NEW_RESP_OF_GROUP": + mail = _NEW_RESP_OF_GROUP + elif mail_type == "RESP_OF_GROUP": + mail = _RESP_OF_GROUP + elif mail_type == "NEW_SEC_OF_GROUP": + mail = _NEW_SEC_OF_GROUP + elif mail_type == "SEC_OF_GROUP": + mail = _SEC_OF_GROUP + else: + raise Exception("Unknown mail type !") + + for key, value in args.items(): + mail[1].replace("#" + key, value) + return mail diff --git a/backend/app/config.py.example b/backend/app/config.py.example index 7319e4c..bcc853d 100644 --- a/backend/app/config.py.example +++ b/backend/app/config.py.example @@ -31,6 +31,7 @@ class Config: CAS_LOGIN_ROUTE = "/login" CAS_LOGOUT_ROUTE = "/logout" CAS_VALIDATE_ROUTE = "/serviceValidate" + MAILER = True @@ -49,3 +50,4 @@ class Test(Config): BASE_DIR = os.path.abspath(os.path.dirname(__file__)) SQLALCHEMY_DATABASE_URI = \ 'sqlite:///' + os.path.join(BASE_DIR, '../test.db') + MAILER = False diff --git a/backend/app/core.py b/backend/app/core.py index 39dab01..2b1ac28 100644 --- a/backend/app/core.py +++ b/backend/app/core.py @@ -1,16 +1,16 @@ import importlib from datetime import timedelta -from flask_cas import CAS - -from app.config import Config from flask import Flask, session, redirect +from flask_cas import CAS from flask_restful import Api from flask_sqlalchemy import SQLAlchemy from sqlalchemy import MetaData from sqlalchemy import create_engine from sqlalchemy.ext.automap import automap_base +from app.config import Config + # initialization Flask app = Flask(__name__) app.config.from_object(Config.ACTIVE_CONFIG) @@ -20,6 +20,7 @@ app.permanent_session_lifetime = \ minutes=app.config['SESSION_VALIDITY_DURATION_WITHOUT_ACTIVITY_MIN'] ) + @app.before_request def before_request(): session.modified = True @@ -37,6 +38,7 @@ api = Api(app) # Cas Flask cas = CAS(app) + @app.route('/redirect') def after_login(): return redirect("/api/login") diff --git a/backend/app/model.py b/backend/app/model.py index c56ae96..70cc838 100644 --- a/backend/app/model.py +++ b/backend/app/model.py @@ -1,11 +1,10 @@ from sqlalchemy import Table -from sqlalchemy import or_ +from sqlalchemy import and_ -from app.core import meta, db, Base +from app.core import meta, Base USER = Table('USER', meta, autoload=False) SETTINGS = Table('SETTINGS', meta, autoload=False) -HASHTABLE = Table('HASHTABLE', meta, autoload=False) GROUP = Table('GROUP', meta, autoload=False) TUTORSHIP = Table('TUTORSHIP', meta, autoload=False) PERIOD = Table('PERIOD', meta, autoload=False) @@ -13,24 +12,26 @@ LIVRET = Table('LIVRET', meta, autoload=False) user_class = Base.classes.USER settings_class = Base.classes.SETTINGS -hashtable_class = Base.classes.HASHTABLE group_class = Base.classes.GROUP tutorship_class = Base.classes.TUTORSHIP period_class = Base.classes.PERIOD livret_class = Base.classes.LIVRET -def getUser(uid=0, login="", email=""): +def getParam(key): + query = SETTINGS.select(SETTINGS.c.key == key) + rows = query.execute() + return rows.first().value + + +def getUser(uid=0, email="", hashcode=""): res = None - if uid == 0 and login == "" and email == "": + if uid == 0 and email == "" and hashcode == "": raise Exception("getUser must be called with one argument !") else: if uid != 0: - res = db.session.query(user_class).get(uid) - - elif login != "": - query = USER.select(USER.c.login == login) + query = USER.select(USER.c.id == uid) rows = query.execute() res = rows.first() @@ -39,14 +40,54 @@ def getUser(uid=0, login="", email=""): rows = query.execute() res = rows.first() + elif hashcode != "": + query = USER.select(USER.c.hash == hashcode) + rows = query.execute() + res = rows.first() + if res is not None: - return {"id": res.id, "login": res.login, "email": res.email, "role": res.role, "phone": res.phone} + return {"id": res.id, "email": res.email, "role": res.role, "phone": res.phone, "name": res.name} else: return None -def isUserAllowed(uid): - query = db.session.query(group_class, tutorship_class).join(tutorship_class) \ - .filter(or_(tutorship_class.student_id == uid, group_class.resp_id == uid)) - res = query.all() - return res is not None and len(res) > 0 +def getGroup(gid=0, name=""): + res = None + + if gid == 0 and name == "": + raise Exception("getGroup must be called with one argument !") + else: + if gid != 0: + query = GROUP.select(GROUP.c.id == gid) + rows = query.execute() + res = rows.first() + + elif name != "": + query = GROUP.select(GROUP.c.name == name) + rows = query.execute() + res = rows.first() + + if res is not None: + return {"id": res.id, "name": res.name, "year": res.year, "class_short": res.class_short, + "class_long": res.class_long, "department": res.department, "resp_id": getUser(uid=res.resp_id), + "sec_id": getUser(uid=res.sec_id), "ressources_dir": res.ressources_dir} + else: + return None + + +def getTutorshipForStudent(gid, student): + query = TUTORSHIP.select(and_(TUTORSHIP.c.group_id == gid, TUTORSHIP.c.student_id == student)) + rows = query.execute() + res = rows.first() + if res is not None: + return {"id": res.id, "group_id": getGroup(gid=res.group_id), "student_id": getUser(uid=res.student_id), + "ptutor_id": getUser(uid=res.ptutor_id)} + else: + return None + + +def hashExists(test): + query = USER.select(USER.c.hash == test) + rows = query.execute() + res = rows.first() + return res is not None diff --git a/backend/app/tools/FusionPdf.py b/backend/app/tools/FusionPdf.py deleted file mode 100644 index 63bcd09..0000000 --- a/backend/app/tools/FusionPdf.py +++ /dev/null @@ -1,25 +0,0 @@ -import os -from PyPDF2 import PdfFileReader, PdfFileMerger - - -def fusion_fichiers(chemin_merge_pdf, nom_merge_pdf, liste_de_pdf): - """ - Permet de fusionner un ensemble de pdf - :param chemin_merge_pdf: chemin ou l'on souhaite fusioner l'ensemble des pdf - :param nom_merge_pdf: nom que l'on souhaite donner au fichier pdf final - :param liste_de_pdf: liste de pdf avec leur chemin inclu - :return: - """ - merger = PdfFileMerger() - for filename in liste_de_pdf: - merger.append(PdfFileReader(os.path.join(filename), "rb")) - merger.write(os.path.join(chemin_merge_pdf, nom_merge_pdf)) - - -def get_pdf_from_directory(chemin_des_pdf): - """ - Permet de récuperer l'ensemble des pdf d'un chemin - :param chemin_des_pdf: - :return: - """ - return [f for f in os.listdir(chemin_des_pdf) if f.endswith("pdf")] diff --git a/backend/app/tools/InsertTemplate.py b/backend/app/tools/InsertTemplate.py deleted file mode 100644 index 6e3f4c9..0000000 --- a/backend/app/tools/InsertTemplate.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -""" Script python qui remplie les pdf basés sur un template jinja. """ - -import os - -from pdfjinja import PdfJinja - - -def remplir_template(dirname_template, pdf_template, dirname_output_file, pdf_output, dictionnaire): - """ - Fonction qui permet de remplir un pdf template - :param dirname_template: chemin du fichier de template - :param pdf_template: nom du fichier de template - :param dirname_output_file: chemin des pdf généré - :param pdf_output: nom du fichier pdf à générer - :param dictionnaire: dictionnaire contenant le nom des textfields des pdf ainsi que leurs valeurs - :return: - """ - template_pdf_file = os.path.join(dirname_template, pdf_template) - template_pdf = PdfJinja(template_pdf_file) - - rendered_pdf = template_pdf(dictionnaire) - - output_file = os.path.join(dirname_output_file, pdf_output) - rendered_pdf.write(open(output_file, 'wb')) diff --git a/backend/app/tools/LibPdf.py b/backend/app/tools/LibPdf.py new file mode 100644 index 0000000..da93aa5 --- /dev/null +++ b/backend/app/tools/LibPdf.py @@ -0,0 +1,69 @@ +import os + +from PyPDF2 import PdfFileReader, PdfFileMerger +from pdfjinja import PdfJinja +from werkzeug.utils import secure_filename + + +def fusion_fichiers(chemin_merge_pdf, nom_merge_pdf, liste_de_pdf): + """ + Permet de fusionner un ensemble de pdf + :param chemin_merge_pdf: chemin ou l'on souhaite fusioner l'ensemble des pdf + :param nom_merge_pdf: nom que l'on souhaite donner au fichier pdf final + :param liste_de_pdf: liste de pdf avec leur chemin inclu + :return: + """ + merger = PdfFileMerger() + for filename in liste_de_pdf: + merger.append(PdfFileReader(os.path.join(filename), "rb")) + merger.write(os.path.join(chemin_merge_pdf, nom_merge_pdf)) + + +def get_pdf_from_directory(chemin_des_pdf): + """ + Permet de récuperer l'ensemble des pdf d'un chemin + :param chemin_des_pdf: + :return: + """ + return [f for f in os.listdir(chemin_des_pdf) if f.endswith("pdf")] + + +def remplir_template(dirname_template, pdf_template, dirname_output_file, pdf_output, dictionnaire): + """ + Fonction qui permet de remplir un pdf template + :param dirname_template: chemin du fichier de template + :param pdf_template: nom du fichier de template + :param dirname_output_file: chemin des pdf généré + :param pdf_output: nom du fichier pdf à générer + :param dictionnaire: dictionnaire contenant le nom des textfields des pdf ainsi que leurs valeurs + :return: + """ + template_pdf_file = os.path.join(dirname_template, pdf_template) + template_pdf = PdfJinja(template_pdf_file) + + rendered_pdf = template_pdf(dictionnaire) + + output_file = os.path.join(dirname_output_file, pdf_output) + rendered_pdf.write(open(output_file, 'wb')) + + +def allowed_file(filename): + allowed_extensions = {'pdf'} + return '.' in filename and \ + filename.rsplit('.', 1)[1].lower() in allowed_extensions + + +def upload_file(file_to_upload, upload_folder): + """ + rep de l'etu avec id + :param file: + :param upload_folder: + :return: + """ + file_to_upload.save(os.path.join(upload_folder, secure_filename(file_to_upload.filename))) + + + +def delete_file(pdf_path): + if os.path.exists(pdf_path): + os.remove(pdf_path) diff --git a/backend/app/urls.py b/backend/app/urls.py index 7f8083e..5ccd60b 100644 --- a/backend/app/urls.py +++ b/backend/app/urls.py @@ -1,12 +1,17 @@ +from app.api.GroupAPI import GroupAPI +from app.api.LivretAPI import LivretAPI +from app.api.LoginAPI import LoginAPI from app.api.UserAPI import UserAPI -from app.api.UserInfoAPI import UserInfoAPI +from app.api.UserInfoAPI import UserInfoAPI, UserGroupsAPI from app.api.exampleapi import SomeApi -from app.api.loginAPI import LoginAPI from app.core import api # Some Api resource api.add_resource(SomeApi, '/api/someapi', '/api/someapi/') api.add_resource(LoginAPI, '/api/login') -api.add_resource(UserAPI, '/api/user', '/api/user/byuid/', '/api/user/bylogin/', - '/api/user/byemail/') api.add_resource(UserInfoAPI, '/api/userInfo') +api.add_resource(UserGroupsAPI, '/api/userGroups') +api.add_resource(UserAPI, '/api/user', '/api/user/byuid/', '/api/user/byemail/', + '/api/user/byhash/') +api.add_resource(GroupAPI, '/api/group', '/api/group/bygid/', '/api/group/byname/') +api.add_resource(LivretAPI, '/api/livret', '/api/livret/byuid/') diff --git a/backend/app/utils.py b/backend/app/utils.py index 06026fc..48c3f64 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -3,8 +3,12 @@ import string from hashlib import sha512 from flask import json +from mailer import Mailer +from mailer import Message from sqlalchemy.ext.declarative import DeclarativeMeta +from app.core import app + SIMPLE_CHARS = string.ascii_letters + string.digits @@ -57,3 +61,12 @@ def new_alchemy_encoder(revisit_self=False, fields_to_expand=[]): def checkParams(wanted, args): inter = [elt for elt in wanted if elt in args] return len(inter) == len(wanted) + + +def send_mail(subject, to, html): + if app.config['MAILER']: + message = Message(From="ola.noreply@univ-tlse2.fr", To=to, charset="utf-8") + message.Subject = subject + message.Html = html + sender = Mailer('localhost') # TODO: Mettre le SMTP de la fac ici + sender.send(message) diff --git a/backend/requirements/common.txt b/backend/requirements/common.txt index d0d77ca..f923bb7 100644 --- a/backend/requirements/common.txt +++ b/backend/requirements/common.txt @@ -3,6 +3,7 @@ flask-script < 2.1 flask-sqlalchemy < 2.2 flask-restful < 0.4 flask-cas +mailer mysqlclient < 1.4 pdfjinja < 1.1 PyPDF2 < 1.27 diff --git a/backend/tests/api/test_Auth.py b/backend/tests/api/test_Auth.py index b13869f..82c0e55 100644 --- a/backend/tests/api/test_Auth.py +++ b/backend/tests/api/test_Auth.py @@ -46,7 +46,7 @@ class AuthTestCase(unittest.TestCase): return self.app.post('/api/login', data=json.dumps( dict( - login=email, + email=email, password=password ) ), content_type='application/json') @@ -58,34 +58,30 @@ class AuthTestCase(unittest.TestCase): return self.app.delete('/api/login') def test_login_logout(self): - rv = self.login('admin', 'admin') + rv = self.login('admin@admin.com', 'admin@admin.com') self.assertEqual(rv.status_code, 200, 'Login as admin Failed') - rv = self.login('admin', 'admin') + rv = self.login('admin@admin.com', 'admin@admin.com') self.assertEqual(rv.status_code, 201, 'Login as admin succeed but should have already been done') rv = self.getUserInfo() self.assertEqual(rv.status_code, 200, 'Getting user info failed') - self.assertEqual({"id": getUser(login="admin")["id"], "login": "admin", "email": "admin@admin.com", "role": 4, + self.assertEqual({"id": getUser(login="admin")["id"], "login": "admin", "email": "admin@admin.com", "role": "4", "phone": "00.00.00.00.00"}, json.loads(rv.data)['USER'], 'Invalid user info') rv = self.logout() self.assertEqual(rv.status_code, 200, 'Logout Failed') - rv = self.login('adminx', 'admin') - self.assertEqual(rv.status_code, 401, 'Authentication from CAS has not failed for the invalid user xadmin !') + rv = self.login('adminx@admin.com', 'admin@admin.com') + self.assertEqual(rv.status_code, 401, 'Authentication not failed for the invalid user!') rv = self.getUserInfo() self.assertEqual(rv.status_code, 200, 'Getting user info failed') self.assertIsNone(json.loads(rv.data)['USER'], 'User info should be None') - rv = self.login('admin', 'adminx') + rv = self.login('admin@admin.com', 'admin@admin.comx') self.assertEqual(rv.status_code, 401, - 'Authentication from CAS has not failed for the invalid password xadmin !') - - rv = self.login('toto', 'toto') - self.assertEqual(rv.status_code, 403, 'Authentication shouldn\'t be allowed for user toto !') - + 'Authenticationnot failed for the invalid password !') if __name__ == '__main__': unittest.main() diff --git a/backend/tests/api/test_Group.py b/backend/tests/api/test_Group.py new file mode 100644 index 0000000..c510f0f --- /dev/null +++ b/backend/tests/api/test_Group.py @@ -0,0 +1,108 @@ +import unittest + +from flask import json + +from app.core import app +from app.model import USER, user_class, GROUP, group_class + + +class GroupTestCase(unittest.TestCase): + uid = None + uid2 = None + + @classmethod + def setUpClass(cls): + query = USER.insert().values(email="admin@admin.com", role="4", phone="00.00.00.00.00", name="admin", + hash="toto") + res = query.execute() + cls.uid = res.lastrowid + query = USER.insert().values(email="adminx@admin.com", role="3", phone="00.00.00.00.00", name="adminx", + hash="zozo") + res = query.execute() + cls.uid2 = res.lastrowid + + @classmethod + def tearDownClass(cls): + query = GROUP.delete().where(group_class.name == "group_test") + query.execute() + query = GROUP.delete().where(group_class.name == "group_test2") + query.execute() + query = USER.delete().where(user_class.email == "admin@admin.com") + query.execute() + query = USER.delete().where(user_class.email == "adminx@admin.com") + query.execute() + + def setUp(self): + self.app = app.test_client() + + def tearDown(self): + pass + + def create_group(self, name, year, class_short, class_long, department, resp_id, sec_id): + return self.app.post('/api/group', + data=json.dumps( + dict( + name=name, + year=year, + class_short=class_short, + class_long=class_long, + department=department, + resp_id=resp_id, + sec_id=sec_id + ) + ), content_type='application/json') + + def getGroupByID(self, GID): + return self.app.get('/api/group/bygid/' + str(GID)) + + def getGroupByName(self, name): + return self.app.get('/api/group/byname/' + name) + + def change_group(self, GID, name, year, class_short, class_long, department, resp_id, sec_id): + return self.app.put('/api/group/bygid/' + str(GID), + data=json.dumps( + dict( + name=name, + year=year, + class_short=class_short, + class_long=class_long, + department=department, + resp_id=resp_id, + sec_id=sec_id + ) + ), content_type='application/json') + + def test_group(self): + rv = self.create_group('group_test', '2017', 'GT', 'GROUP_TEST', 'TESTING', self.uid, self.uid2) + self.assertEqual(rv.status_code, 201, 'Creating group Failed') + gid = json.loads(rv.data)['GID'] + self.assertIsNotNone(gid) + + rv = self.create_group('group_test', '2017', 'GT', 'GROUP_TEST', 'TESTING', self.uid, self.uid2) + self.assertEqual(rv.status_code, 200, 'Group is supposed to already exist') + gid2 = json.loads(rv.data)['GID'] + self.assertEqual(gid, gid2, "The GID must be the same !") + + rv = self.getGroupByID(gid) + self.assertEqual(rv.status_code, 200, 'Getting group failed by ID') + group = json.loads(rv.data)['GROUP'] + self.assertIsNotNone(group) + + rv = self.getGroupByName("group_test") + self.assertEqual(rv.status_code, 200, 'Getting group failed by Name') + group2 = json.loads(rv.data)['GROUP'] + self.assertEqual(group, group2, "Group by name must be the same !") + + rv = self.change_group(gid, 'group_test2', '2018', 'GT2', 'GROUP_TEST2', 'TESTING2', self.uid2, self.uid) + self.assertEqual(rv.status_code, 200, 'Group modification failed !') + gid3 = json.loads(rv.data)['GID'] + self.assertEqual(gid, gid3, "GIDs doesn't match !") + + rv = self.getGroupByName('group_test2') + self.assertEqual(rv.status_code, 200, 'Getting modified group failed by Name') + group4 = json.loads(rv.data)['GROUP'] + self.assertIsNotNone(group4, "Modified group shouldn't be None !") + + +if __name__ == '__main__': + unittest.main() diff --git a/backend/tests/api/test_User.py b/backend/tests/api/test_User.py new file mode 100644 index 0000000..435523b --- /dev/null +++ b/backend/tests/api/test_User.py @@ -0,0 +1,88 @@ +import unittest + +from flask import json + +from app.core import app +from app.model import USER, user_class + + +class UserTestCase(unittest.TestCase): + uid = None + gid = None + tid = None + + @classmethod + def tearDownClass(cls): + query = USER.delete().where(user_class.email == "admin@admin.com") + query.execute() + query = USER.delete().where(user_class.email == "adminx@admin.com") + query.execute() + + def setUp(self): + self.app = app.test_client() + + def tearDown(self): + pass + + def create_user(self, email, role, name): + return self.app.post('/api/user', + data=json.dumps( + dict( + email=email, + role=role, + name=name + ) + ), content_type='application/json') + + def getUserByID(self, UID): + return self.app.get('/api/user/byuid/' + str(UID)) + + def getUserByEmail(self, email): + return self.app.get('/api/user/byemail/' + email) + + def change_user(self, UID, email, role, phone, name, password): + return self.app.put('/api/user/byuid/' + str(UID), + data=json.dumps( + dict( + role=role, + email=email, + phone=phone, + name=name, + password=password + ) + ), content_type='application/json') + + def test_user(self): + rv = self.create_user('admin@admin.com', '4', 'Admin') + self.assertEqual(rv.status_code, 201, 'Creating user Failed') + uid = json.loads(rv.data)['UID'] + self.assertIsNotNone(uid) + + rv = self.create_user('admin@admin.com', '4', 'Admin') + self.assertEqual(rv.status_code, 200, 'User is supposed to already exist') + uid2 = json.loads(rv.data)['UID'] + self.assertEqual(uid, uid2, "The UID must be the same !") + + rv = self.getUserByID(uid) + self.assertEqual(rv.status_code, 200, 'Getting user failed by ID') + user = json.loads(rv.data)['USER'] + self.assertIsNotNone(user) + + rv = self.getUserByEmail("admin@admin.com") + self.assertEqual(rv.status_code, 200, 'Getting user failed by email') + user3 = json.loads(rv.data)['USER'] + self.assertEqual(user, user3, "User by email must be the same !") + + rv = self.change_user(uid, 'adminx@admin.com', '3', '11.11.11.11.11', 'Adminx', 'password') + self.assertEqual(rv.status_code, 200, 'User modification failed !') + uid3 = json.loads(rv.data)['UID'] + self.assertEqual(uid, uid3, "UIDs doesn't match !") + + rv = self.getUserByEmail("adminx@admin.com") + self.assertEqual(rv.status_code, 200, 'Getting modified user failed by Email') + user4 = json.loads(rv.data)['USER'] + self.assertIsNotNone(user4, "Modified user shouldn't be None !") + + +if __name__ == '__main__': + unittest.main() diff --git a/backend/tests/tools/PyPDF2/__init__.py b/backend/tests/tools/libPdf/PyPDF2/__init__.py similarity index 100% rename from backend/tests/tools/PyPDF2/__init__.py rename to backend/tests/tools/libPdf/PyPDF2/__init__.py diff --git a/backend/tests/tools/PyPDF2/page1.pdf b/backend/tests/tools/libPdf/PyPDF2/page1.pdf similarity index 100% rename from backend/tests/tools/PyPDF2/page1.pdf rename to backend/tests/tools/libPdf/PyPDF2/page1.pdf diff --git a/backend/tests/tools/PyPDF2/page2.pdf b/backend/tests/tools/libPdf/PyPDF2/page2.pdf similarity index 100% rename from backend/tests/tools/PyPDF2/page2.pdf rename to backend/tests/tools/libPdf/PyPDF2/page2.pdf diff --git a/backend/tests/tools/PyPDF2/testFusionFichiers.py b/backend/tests/tools/libPdf/PyPDF2/testFusionFichiers.py similarity index 99% rename from backend/tests/tools/PyPDF2/testFusionFichiers.py rename to backend/tests/tools/libPdf/PyPDF2/testFusionFichiers.py index 0fb6737..02cccaf 100644 --- a/backend/tests/tools/PyPDF2/testFusionFichiers.py +++ b/backend/tests/tools/libPdf/PyPDF2/testFusionFichiers.py @@ -1,9 +1,7 @@ import os import unittest - -from pathlib import Path - from builtins import print +from pathlib import Path from app.tools.FusionPdf import fusion_fichiers, get_pdf_from_directory diff --git a/backend/tests/tools/libPdf/UploadPDF/page1.pdf b/backend/tests/tools/libPdf/UploadPDF/page1.pdf new file mode 100644 index 0000000..6b4e9cc Binary files /dev/null and b/backend/tests/tools/libPdf/UploadPDF/page1.pdf differ diff --git a/backend/tests/tools/libPdf/UploadPDF/testUploadPdf.py b/backend/tests/tools/libPdf/UploadPDF/testUploadPdf.py new file mode 100644 index 0000000..45e1940 --- /dev/null +++ b/backend/tests/tools/libPdf/UploadPDF/testUploadPdf.py @@ -0,0 +1,20 @@ +import os +import unittest + +from werkzeug.datastructures import FileStorage + +from app.tools.LibPdf import upload_file + + +class TestFusionTestCase(unittest.TestCase): + def setUp(self): + self.datadir = os.path.join(os.path.dirname(__file__)) + + def test_fusion(self): + with open("page1.pdf", 'rb') as fp: + file = FileStorage(fp) + upload_file(file, "upload") + + # self.assertTrue(Path(self.datadir + "/testFusion.pdf").is_file(), "Pdf fusionne inexistant") + # self.assertTrue(len(get_pdf_from_directory(self.datadir)) > 0, "pdf non trouve") + # os.remove(self.datadir + "/testFusion.pdf") diff --git a/backend/tests/tools/pdfjinja/__init__.py b/backend/tests/tools/libPdf/__init__.py similarity index 100% rename from backend/tests/tools/pdfjinja/__init__.py rename to backend/tests/tools/libPdf/__init__.py diff --git a/backend/tests/tools/libPdf/pdfjinja/__init__.py b/backend/tests/tools/libPdf/pdfjinja/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/tools/pdfjinja/sample.pdf b/backend/tests/tools/libPdf/pdfjinja/sample.pdf similarity index 100% rename from backend/tests/tools/pdfjinja/sample.pdf rename to backend/tests/tools/libPdf/pdfjinja/sample.pdf diff --git a/backend/tests/tools/pdfjinja/sample_backup.pdf b/backend/tests/tools/libPdf/pdfjinja/sample_backup.pdf similarity index 100% rename from backend/tests/tools/pdfjinja/sample_backup.pdf rename to backend/tests/tools/libPdf/pdfjinja/sample_backup.pdf diff --git a/backend/tests/tools/pdfjinja/testInsertTemplate.py b/backend/tests/tools/libPdf/pdfjinja/testInsertTemplate.py similarity index 99% rename from backend/tests/tools/pdfjinja/testInsertTemplate.py rename to backend/tests/tools/libPdf/pdfjinja/testInsertTemplate.py index 68911c2..9cf6078 100644 --- a/backend/tests/tools/pdfjinja/testInsertTemplate.py +++ b/backend/tests/tools/libPdf/pdfjinja/testInsertTemplate.py @@ -3,9 +3,8 @@ import unittest from io import BytesIO from pathlib import Path -from pdfjinja import PdfJinja - from app.tools.InsertTemplate import remplir_template +from pdfjinja import PdfJinja class InsertTemplateTestCase(unittest.TestCase): diff --git a/frontend/app/index.html b/frontend/app/index.html index f2a7302..40c119b 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -46,6 +46,8 @@ + + @@ -54,6 +56,10 @@ + + + + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 70ad11e..c660cb5 100755 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -14,7 +14,8 @@ var app = angular.module('clientApp', [ 'ngSanitize', 'ngMaterial', 'ui.router', - 'ngMdIcons' + 'ngMdIcons', + 'angularFileUpload' ]); app.config(function ($stateProvider, $urlRouterProvider) { @@ -37,10 +38,16 @@ app.config(function ($stateProvider, $urlRouterProvider) { templateUrl: 'views/studentSpace.html', controller: 'StudentSpaceCtrl' }) - + .state('responsableFormationSpace', { url: '/espace-formation', templateUrl: 'views/responsableFormationSpace.html', controller: 'ResponsableFormationSpaceCtrl' + }) + + .state('administrationSpace', { + url: '/espace-secretariat', + templateUrl: 'views/administrationSpace.html', + controller: 'AdministrationSpaceCtrl' }); }); \ No newline at end of file diff --git a/frontend/app/scripts/controllers/administrationDialog.js b/frontend/app/scripts/controllers/administrationDialog.js new file mode 100644 index 0000000..c76a7a2 --- /dev/null +++ b/frontend/app/scripts/controllers/administrationDialog.js @@ -0,0 +1,104 @@ +(function () { + 'use strict'; + + /** + * @ngdoc function + * @name frontendApp.controller:AdministrationDialogCtrl + * @description + * # AdministrationDialogCtrl + * Controller of the frontendApp + */ + angular.module('clientApp') + .controller('AdministrationDialogCtrl', function ($scope, $state, FileUploader, $mdDialog, fileNameFilter, illegalFileNamesFilter, type) { + + console.log(type); + + // Public methods ------------------- + + $scope.hide = function () { + $mdDialog.hide(); + }; + + $scope.cancel = function () { + $mdDialog.cancel(); + }; + + $scope.answer = function (answer) { + $mdDialog.hide(answer); + }; + + $scope.allDocumentsAreIllegal = function() { + return (fileNameFilter(uploader.queue, type).length === 0); + }; + + $scope.areThereIllegalFiles = function() { + return (illegalFileNamesFilter(uploader.queue, type).length !== 0); + }; + + var uploader = $scope.uploader = new FileUploader({ + url: 'upload.php' + }); + + // Private methods ------------------ + + // FILTERS + + // a sync filter + uploader.filters.push({ + name: 'syncFilter', + fn: function (item /*{File|FileLikeObject}*/, options) { + console.log('syncFilter'); + return this.queue.length < 10; + } + }); + + // an async filter + uploader.filters.push({ + name: 'asyncFilter', + fn: function (item /*{File|FileLikeObject}*/, options, deferred) { + console.log('asyncFilter'); + setTimeout(deferred.resolve, 1e3); + } + }); + + // CALLBACKS + + uploader.onWhenAddingFileFailed = function (item /*{File|FileLikeObject}*/, filter, options) { + console.info('onWhenAddingFileFailed', item, filter, options); + }; + uploader.onAfterAddingFile = function (fileItem) { + console.info('onAfterAddingFile', fileItem); + }; + uploader.onAfterAddingAll = function (addedFileItems) { + console.info('onAfterAddingAll', addedFileItems); + }; + uploader.onBeforeUploadItem = function (item) { + console.info('onBeforeUploadItem', item); + }; + uploader.onProgressItem = function (fileItem, progress) { + console.info('onProgressItem', fileItem, progress); + }; + uploader.onProgressAll = function (progress) { + console.info('onProgressAll', progress); + }; + uploader.onSuccessItem = function (fileItem, response, status, headers) { + console.info('onSuccessItem', fileItem, response, status, headers); + }; + uploader.onErrorItem = function (fileItem, response, status, headers) { + console.info('onErrorItem', fileItem, response, status, headers); + }; + uploader.onCancelItem = function (fileItem, response, status, headers) { + console.info('onCancelItem', fileItem, response, status, headers); + }; + uploader.onCompleteItem = function (fileItem, response, status, headers) { + console.info('onCompleteItem', fileItem, response, status, headers); + }; + uploader.onCompleteAll = function () { + console.info('onCompleteAll'); + }; + + console.info('uploader', uploader); + + }); + +})(); \ No newline at end of file diff --git a/frontend/app/scripts/controllers/administrationSpace.js b/frontend/app/scripts/controllers/administrationSpace.js new file mode 100644 index 0000000..220feb0 --- /dev/null +++ b/frontend/app/scripts/controllers/administrationSpace.js @@ -0,0 +1,125 @@ +(function () { + 'use strict'; + + /** + * @ngdoc function + * @name frontendApp.controller:AdministrationSpaceCtrl + * @description + * # AdministrationSpaceCtrl + * Controller of the frontendApp + */ + angular.module('clientApp') + .controller('AdministrationSpaceCtrl', function ($scope, $state, $mdDialog, FileUploader) { + + angular.extend($scope, { + logout, + deleteAbsence, + deleteTrackingSheet, + importAbsences, + importVisitSheets + }) + + init(); + + // Public methods ------------------- + + function logout() { + $state.go('login'); + } + + function deleteAbsence(groupIndex, periodIndex, absenceIndex) { + $scope.formationGroups[groupIndex].formattedAbsences[periodIndex].absences.splice(absenceIndex, 1); + } + + function deleteTrackingSheet(groupIndex, trackingSheetIndex) { + $scope.formationGroups[groupIndex].trackingSheets.splice(trackingSheetIndex, 1); + } + + function importVisitSheets(ev) { + $mdDialog.show({ + controller: 'AdministrationDialogCtrl', + templateUrl: 'import-fiches-visite', + parent: angular.element(document.body), + targetEvent: ev, + clickOutsideToClose: true, + fullscreen: 'false', + locals : { type : 'visit'} + }) + .then(function (answer) { + $scope.status = 'You said the information was "' + answer + '".'; + }, function () { + $scope.status = 'You cancelled the dialog.'; + }); + } + + function importAbsences(ev) { + $mdDialog.show({ + controller: 'AdministrationDialogCtrl', + templateUrl: 'import-fiches-absences', + parent: angular.element(document.body), + targetEvent: ev, + clickOutsideToClose: true, + fullscreen: 'false', + locals : { type : 'absence'} + }) + .then(function (answer) { + $scope.status = 'You said the information was "' + answer + '".'; + }, function () { + $scope.status = 'You cancelled the dialog.'; + }); + } + + // Private methods ------------------ + + function init() { + var formationGroups = [{ + label: "Master2 ICE", + absences: [ + { id: 1, title: "Absence_Matthieu_Penchenat_P1" }, + { id: 2, title: "Absence_Renan_Husson_P1" }, + { id: 3, title: "Absence_Renan_Husson_P2" }, + { id: 1, title: "Absence_Renan_Husson_P3" }, + { id: 2, title: "Absence_Matthieu_Penchenat_P2" }, + { id: 3, title: "Absence_Matthieu_Penchenat_P3" }, + { id: 1, title: "Absence_Quentin_Rouland_P1" }, + { id: 2, title: "Absence_Quentin_Rouland_P2" }, + { id: 3, title: "Absence_Quentin_Rouland_P3" }, + { id: 1, title: "Absence_Sitan_Coulibaly_P1" }, + { id: 2, title: "Absence_Sitan_Coulibaly_P2" }, + { id: 3, title: "Absence_Sitan_Coulibaly_P3" } + ], + trackingSheets: [ + { id: 3, fileName: "FicheVisite_Sitan_Coulibaly_1" }, + { id: 2, fileName: "FicheVisite_Sitan_Coulibaly_2" }, + { id: 1, fileName: "FicheVisite_Sitan_Coulibaly_3" } + ] + }, { + label: "Master1 ISMAG", + absences: [ + { id: 1, title: "Absence_Matthieu_Penchenat_P1" }, + { id: 2, title: "Absence_Matthieu_Penchenat_P2" }, + { id: 3, title: "Absence_Matthieu_Penchenat_P3" } + ], + trackingSheets: [ + { id: 3, fileName: "FicheVisite_Renan_Husson_1" }, + { id: 2, fileName: "FicheVisite_Renan_Husson_2" }, + { id: 1, fileName: "FicheVisite_Renan_Husson_3" } + ] + }]; + + $scope.formationGroups = formationGroups.map(function (formationGroup) { + formationGroup.formattedAbsences = reformatAbsences(formationGroup.absences); + return formationGroup; + }); + } + + function reformatAbsences(absences) { + var myObj = _.groupBy(absences, function (absence) { return absence.title.split('_').pop(); }); + + return _.map(myObj, function (value, index) { + return { period: index, absences: value }; + }); + } + }); + +})(); \ No newline at end of file diff --git a/frontend/app/scripts/services/Filters.js b/frontend/app/scripts/services/Filters.js new file mode 100644 index 0000000..cc1312f --- /dev/null +++ b/frontend/app/scripts/services/Filters.js @@ -0,0 +1,28 @@ +(function () { + 'use strict'; + + + angular.module('clientApp') + .filter('fileName', function () { + + return function (queue, type) { + + var reg = (type === 'absence') ?/^Absence_[A-Z][a-z]*_[A-Z][a-z]*_P\d*.pdf$/ : /^Visite_[A-Z][a-z]*_[A-Z][a-z]*_\d*.pdf$/; + return queue.filter(function (item) { + return reg.test(item.file.name); + }); + }; + }) + .filter('illegalFileNames', function () { + + return function (queue, type) { + + var reg = (type === 'absence') ?/^Absence_[A-Z][a-z]*_[A-Z][a-z]*_P\d*.pdf$/ : /^Visite_[A-Z][a-z]*_[A-Z][a-z]*_\d*.pdf$/; + return queue.filter(function (item) { + return !reg.test(item.file.name); + }).map(function(item) { + return item.file.name; + }); + }; + }); +})(); \ No newline at end of file diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index b4e0e9b..115e6f1 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -74,4 +74,24 @@ md-tab-data{ padding: 8px; /* border: 1px solid; */ background-color: yellowgreen; -} \ No newline at end of file +} + +.well { + padding: 15px; + margin: 5px; + background-color: #e0e0e0; + border: dotted 3px lightgray; +} + +.note { + padding: 15px; + margin: 5px; + background-color: #a5d6a7; +} + + +.error { + padding: 15px; + margin: 5px; + background-color: #ef9a9a; +} diff --git a/frontend/app/views/administrationSpace.html b/frontend/app/views/administrationSpace.html new file mode 100644 index 0000000..a1f1dcb --- /dev/null +++ b/frontend/app/views/administrationSpace.html @@ -0,0 +1,304 @@ +
+ + +
+

Bienvenue Isabelle Michu

+ + + Se déconnecter + +
+ +
+
+ +
+ +
+ + + + +
+ + + + Fiches d'absence + + + + +
+
+ Période {{formattedAbsence.period}} + +

{{absence.title}}

+ +
+
+
+
+
+ Importer des fiches d'absences +
+
+
+
+ + + + + Fiches de visite + + + + +
+ +

{{sheet.fileName}}

+ +
+
+
+
+ Importer des fiches de visite +
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/frontend/app/views/login.html b/frontend/app/views/login.html index 6b3e72c..0519410 100644 --- a/frontend/app/views/login.html +++ b/frontend/app/views/login.html @@ -8,6 +8,8 @@ + + Log to administration

The titles of Washed Out's breakthrough song and the first single from Paracosm share the two most important words in Ernest Greene's musical language: feel it. It's a simple request, as well... diff --git a/frontend/app/views/studentSpace.html b/frontend/app/views/studentSpace.html index b3c3933..04abe97 100755 --- a/frontend/app/views/studentSpace.html +++ b/frontend/app/views/studentSpace.html @@ -13,37 +13,209 @@

-
- - - -
-
-
- Remove Tab + + +
+ + + + + + +
+ Exporter le livret +
+ + + + +

Période {{period.number}}

+
+
+ + + +
+
+

+ Université + + + +

+ + +

Lorem ipsum dolor sit amet, ne quod novum mei. Sea omnium invenire mediocrem at, in lobortis conclusionemque + nam. Ne deleniti appetere reprimique pro, inani labitur disputationi te sed. At vix sale omnesque, + id pro labitur reformidans accommodare, cum labores honestatis eu. Nec quem lucilius in, eam praesent + reformidans no. Sed laudem aliquam ne.


+ + + + + + +
+ Enregistrer +
+
+
+ +
+

+ Entreprise + + + +

+ + +

Lorem ipsum dolor sit amet, ne quod novum mei. Sea omnium invenire mediocrem at, in lobortis conclusionemque + nam. Ne deleniti appetere reprimique pro, inani labitur disputationi te sed. At vix sale omnesque, + id pro labitur reformidans accommodare, cum labores honestatis eu. Nec quem lucilius in, eam praesent + reformidans no. Sed laudem aliquam ne.

+ + + + + + +
+ Enregistrer +
+
+
+
+
+
+ +
+
+ + + +
+ + + +

Informations personnelles

+
+
+ + +
+ + + + + + + + + +
+ +
+ + + + Apprentissage + Professionnalisation + Stage + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+
+ + + + +

Tuteur pédagogique

+
+
+ + + + + + + + +
+ + + + +

Tuteur entreprise

+
+
+ + +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ + + + + + +
+
+ +
+ Enregistrer +
+
+
+
+ +
+
- - -
- -
-
-
-

Add a new Tab:

-
- - - - - - - - - Add Tab -
-
- -
-
\ No newline at end of file + +
\ No newline at end of file diff --git a/frontend/bower.json b/frontend/bower.json index fc52ffd..c56f888 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -8,7 +8,9 @@ "angular-sanitize": "^1.4.0", "angular-material": "^1.1.3", "angular-ui-router": "^0.4.2", - "angular-material-icons": "^0.7.1" + "angular-material-icons": "^0.7.1", + "underscore": "^1.8.3", + "angular-file-upload": "^2.5.0" }, "devDependencies": { "angular-mocks": "^1.4.0" diff --git a/frontend/test/karma.conf.js b/frontend/test/karma.conf.js index a076726..7f9e36f 100644 --- a/frontend/test/karma.conf.js +++ b/frontend/test/karma.conf.js @@ -29,6 +29,8 @@ module.exports = function(config) { 'bower_components/angular-material/angular-material.js', 'bower_components/angular-ui-router/release/angular-ui-router.js', 'bower_components/angular-material-icons/angular-material-icons.min.js', + 'bower_components/underscore/underscore.js', + 'bower_components/angular-file-upload/dist/angular-file-upload.min.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower 'app/scripts/**/*.js',