This commit is contained in:
ariasia 2017-03-30 13:20:10 +00:00
commit 8c97b49a67
62 changed files with 2030 additions and 231 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
.idea
.directory

View File

@ -20,20 +20,13 @@ GET -> Get the current logged user, return None if no one is connected
Out:
200 -> USER = <USER_OBJECT>|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 = <USER_ID> : The user already exists with the id USER_ID
201 -> UID = <USER_ID> : 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/<USER_ID>)
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 = <USER_ID> : 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/<USER_ID> | /bylogin/<USER_LOGIN> | /byemail/<USER_EMAIL>)
In: (Suffixes = /byuid/<USER_ID> | /byemail/<USER_EMAIL>)
Out:
200 -> USER = <USER_OBJECT>|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 = <GROUP_ID> : The group already exists with the id GROUP_ID
201 -> GID = <GROUP_ID> : 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 <USER_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/<GROUP_ID>)
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 = <GROUP_ID> : 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 <USER_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/<GROUP_ID> | /byname/<GROUP_NAME> )
Out:
200 -> GROUP = <GROUP_OBJECT>|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 <USER_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

View File

@ -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 }}

1
backend/.gitignore vendored
View File

@ -31,3 +31,4 @@ coverage.xml
#Config files
config.py
/app/OLA_RESSOURCES/

View File

@ -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`);

25
backend/OLA_DATA.mysql Normal file
View File

@ -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);

View File

@ -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);

221
backend/app/api/GroupAPI.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

43
backend/app/api/PdfAPI.py Normal file
View File

@ -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"])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,53 @@
_NEW_STUD_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,<br/><p>Votre compte vient d'être créé dans l'Outil du "
"Livret de l'Alternant dans le groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant l'activer, puis créer un livret en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
_STUD_OF_GROUP = (
"Vous avez été ajouté à un groupe OLA !", "Bonjour,<br/><p>Votre compte vient d'être ajouté dans l'Outil du "
"Livret de l'Alternant au groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant créer un livret en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
_NEW_RESP_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,<br/><p>Votre compte vient d'être créé dans l'Outil du "
"Livret de l'Alternant en tant que responsable du groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant l'activer, en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
_RESP_OF_GROUP = (
"Vous avez été ajouté à un groupe OLA !", "Bonjour,<br/><p>Votre compte vient d'être ajouté dans l'Outil du "
"Livret de l'Alternant en tant que responsable du groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant y accéder en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
_NEW_SEC_OF_GROUP = ("Votre compte OLA a été créé !", "Bonjour,<br/><p>Votre compte vient d'être créé dans l'Outil du "
"Livret de l'Alternant en tant que secrétaire du groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant l'activer, en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
_SEC_OF_GROUP = (
"Vous avez été ajouté à un groupe OLA !", "Bonjour,<br/><p>Votre compte vient d'être ajouté dans l'Outil du "
"Livret de l'Alternant en tant que secrétaire du groupe <b>#GROUPE</b>. Vous pouvez dès "
"maintenant y accéder en vous rendant à l'adresse : <br/>"
"<a href='#URL'>#URL</a></p><p>Bonne journée !</p>")
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

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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")]

View File

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

View File

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

View File

@ -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/<int:id>')
api.add_resource(LoginAPI, '/api/login')
api.add_resource(UserAPI, '/api/user', '/api/user/byuid/<int:uid>', '/api/user/bylogin/<string:login>',
'/api/user/byemail/<string:email>')
api.add_resource(UserInfoAPI, '/api/userInfo')
api.add_resource(UserGroupsAPI, '/api/userGroups')
api.add_resource(UserAPI, '/api/user', '/api/user/byuid/<int:uid>', '/api/user/byemail/<string:email>',
'/api/user/byhash/<string:hashcode>')
api.add_resource(GroupAPI, '/api/group', '/api/group/bygid/<int:gid>', '/api/group/byname/<string:name>')
api.add_resource(LivretAPI, '/api/livret', '/api/livret/byuid/<int:uid>')

View File

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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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

Binary file not shown.

View File

@ -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")

View File

@ -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):

View File

@ -46,6 +46,8 @@
<script src="bower_components/angular-material/angular-material.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script src="bower_components/angular-material-icons/angular-material-icons.min.js"></script>
<script src="bower_components/underscore/underscore.js"></script>
<script src="bower_components/angular-file-upload/dist/angular-file-upload.min.js"></script>
<!-- endbower -->
<!-- endbuild -->
@ -54,6 +56,10 @@
<script src="scripts/controllers/login.js"></script>
<script src="scripts/controllers/studentSpace.js"></script>
<script src="scripts/controllers/responsableFormationSpace.js"></script>
<script src="scripts/controllers/administrationSpace.js"></script>
<script src="scripts/controllers/administrationDialog.js"></script>
<script src="scripts/services/Filters.js"></script>
<!-- endbuild -->
</body>
</html>

View File

@ -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'
});
});

View File

@ -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);
});
})();

View File

@ -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 };
});
}
});
})();

View File

@ -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;
});
};
});
})();

View File

@ -74,4 +74,24 @@ md-tab-data{
padding: 8px;
/* border: 1px solid; */
background-color: yellowgreen;
}
}
.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;
}

View File

@ -0,0 +1,304 @@
<div layout="column">
<md-toolbar>
<div class="md-toolbar-tools">
<h2 md-truncate flex>Bienvenue Isabelle Michu</h2>
<md-button ng-click="logout()">
Se déconnecter
</md-button>
</div>
</md-toolbar>
</div>
<div layout="row" layout-margin>
<div ng-cloak flex="100">
<md-content>
<md-tabs md-dynamic-height md-border-bottom>
<md-tab ng-repeat="formationGroup in formationGroups" label="{{formationGroup.label}}">
<div layout="row" layout-margin>
<md-card flex="50">
<md-card-title>
<md-card-title-text>
<span class="md-headline">Fiches d'absence</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<md-content flex layout-padding>
<div style="height: 350px; overflow: auto">
<div ng-repeat="formattedAbsence in formationGroup.formattedAbsences">
<md-subheader class="md-no-sticky">Période {{formattedAbsence.period}}</md-subheader>
<md-list-item ng-repeat="absence in formattedAbsence.absences">
<p>{{absence.title}}</p>
<ng-md-icon icon="delete" style="fill: grey" size="24" ng-click="deleteAbsence($parent.$parent.$index, $parent.$index, $index)"></ng-md-icon>
</md-list-item>
</div>
</div>
<br />
<center>
<md-button class="md-raised md-primary" ng-click="importAbsences($event)">Importer des fiches d'absences</md-button>
</center>
</md-content>
</md-card-content>
</md-card>
<md-card flex="50">
<md-card-title>
<md-card-title-text>
<span class="md-headline">Fiches de visite</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<md-content flex layout-padding>
<div style="height: 350px; overflow: auto">
<md-list-item ng-repeat="sheet in formationGroup.trackingSheets">
<p>{{sheet.fileName}}</p>
<ng-md-icon icon="delete" style="fill: grey" size="24" ng-click="deleteTrackingSheet($parent.$index, $index)"></ng-md-icon>
</md-list-item>
</div>
<br />
<center>
<md-button class="md-raised md-primary" ng-click="importVisitSheets($event)">Importer des fiches de visite</md-button>
</center>
</md-content>
</md-card-content>
</md-card>
</div>
</md-tab>
</md-tabs>
</md-content>
</div>
</div>
<script type="text/ng-template" id="import-fiches-absences">
<md-dialog aria-label="Importer des fiches d'absences">
<form ng-cloak>
<md-toolbar>
<div class="md-toolbar-tools">
<h2>Importer des fiches d'absences</h2>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content">
<div layout="row">
<div flex>
<h3>Importer des fiches d'absences</h3>
<input type="file" nv-file-select="" accept=".pdf" uploader="uploader" multiple /><br/><br/>
<p class="note">
<ng-md-icon icon="warning" size="20"></ng-md-icon>
Note d'utilisation : chaque fiche d'absence doit respecter une règle de nommage.<br/> Nom
de fichier : Absence_Prenom_Nom_PN.pdf<br/>
<i>Le N de PN étant un numéro - exemple : (P1, P2, P3, etc.)</i>
</p>
<div ng-if="uploader.queue.length !== 0">
<h3>Liste des documents : </h3>
<p>Nombre de documents : {{ uploader.queue.length }}</p>
<div ng-if="areThereIllegalFiles()" style="background-color: #ef9a9a; padding: 2px 10px 2px 10px;margin: 10px 0 10px 0">
<p>
Les documents suivants ne respectent pas la règle de nommage :
</p>
<ul>
<li ng-repeat="fileName in uploader.queue | illegalFileNames:'absence'">{{fileName}}</li>
</ul>
</div>
<div ng-if="!allDocumentsAreIllegal()">
<table class="bordered">
<thead>
<tr>
<th width="50%">Nom</th>
<th ng-show="uploader.isHTML5">Taille</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in uploader.queue | fileName:'absence'">
<td><strong>{{ item.file.name }}</strong></td>
<td ng-show="uploader.isHTML5" nowrap>{{ item.file.size/1024/1024|number:2 }} MB</td>
<td class="text-center">
<span ng-show="item.isSuccess">
<ng-md-icon icon="done" size="24"></ng-md-icon>
</span>
<span ng-show="item.isCancel">
<ng-md-icon icon="cancel" size="24"></ng-md-icon>
</span>
<span ng-show="item.isError">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon>
</span>
</td>
<td nowrap>
<md-button type="button" class="md-raised md-primary" ng-click="item.upload()" ng-disabled="item.isReady || item.isUploading || item.isSuccess">
<ng-md-icon icon="file_upload" size="24"></ng-md-icon> Charger
</md-button>
<md-button type="button" class="md-raised" ng-click="item.cancel()" ng-disabled="!item.isUploading">
<ng-md-icon icon="cancel" size="24"></ng-md-icon> Annuler
</md-button>
<md-button type="button" class="md-raised md-warn" ng-click="item.remove()">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon> Supprimer
</md-button>
</td>
</tr>
</tbody>
</table>
<div style='margin-top : 40px'>
<div>
Niveau de progression :
<md-progress-linear md-mode="determinate" value="{{uploader.progress}}"></md-progress-linear>
</div>
<md-button type="button" class="md-raised md-primary" ng-click="uploader.uploadAll()" ng-disabled="!uploader.getNotUploadedItems().length">
<ng-md-icon icon="file_upload" size="24"></ng-md-icon> Tout charger
</md-button>
<md-button type="button" class="md-raised" ng-click="uploader.cancelAll()" ng-disabled="!uploader.isUploading">
<ng-md-icon icon="cancel" size="24"></ng-md-icon> Tout annuler
</md-button>
<md-button type="button" class="md-raised md-warn" ng-click="uploader.clearQueue()" ng-disabled="!uploader.queue.length">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon> Tout supprimer
</md-button>
</div>
</div>
</div>
</div>
</div>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="answer('useful')">
Fermer
</md-button>
</md-dialog-actions>
</form>
</md-dialog>
</script>
<script type="text/ng-template" id="import-fiches-visite">
<md-dialog aria-label="Importer des fiches de visite">
<form ng-cloak>
<md-toolbar>
<div class="md-toolbar-tools">
<h2>Importer des fiches de visite</h2>
</div>
</md-toolbar>
<md-dialog-content>
<div class="md-dialog-content">
<div layout="row">
<div flex>
<h3>Importer des fiches de visite</h3>
<input type="file" nv-file-select="" accept=".pdf" uploader="uploader" multiple /><br/><br/>
<p class="note">
<ng-md-icon icon="warning" size="20"></ng-md-icon>
Note d'utilisation : chaque fiche de visite doit respecter une règle de nommage.<br/> Nom
de fichier : Visite_Prenom_Nom_N.pdf<br/>
<i>Le N étant un numéro.</i>
</p>
<div ng-if="uploader.queue.length !== 0">
<h3>Liste des documents : </h3>
<p>Nombre de documents : {{ uploader.queue.length }}</p>
<div ng-if="areThereIllegalFiles()" style="background-color: #ef9a9a; padding: 2px 10px 2px 10px;margin: 10px 0 10px 0">
<p>
Les documents suivants ne respectent pas la règle de nommage :
</p>
<ul>
<li ng-repeat="fileName in uploader.queue | illegalFileNames:'visit'">{{fileName}}</li>
</ul>
</div>
<div ng-if="!allDocumentsAreIllegal()">
<table class="bordered">
<thead>
<tr>
<th width="50%">Nom</th>
<th ng-show="uploader.isHTML5">Taille</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="item in uploader.queue | fileName:'visit'">
<td><strong>{{ item.file.name }}</strong></td>
<td ng-show="uploader.isHTML5" nowrap>{{ item.file.size/1024/1024|number:2 }} MB</td>
<td class="text-center">
<span ng-show="item.isSuccess">
<ng-md-icon icon="done" size="24"></ng-md-icon>
</span>
<span ng-show="item.isCancel">
<ng-md-icon icon="cancel" size="24"></ng-md-icon>
</span>
<span ng-show="item.isError">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon>
</span>
</td>
<td nowrap>
<md-button type="button" class="md-raised md-primary" ng-click="item.upload()" ng-disabled="item.isReady || item.isUploading || item.isSuccess">
<ng-md-icon icon="file_upload" size="24"></ng-md-icon> Charger
</md-button>
<md-button type="button" class="md-raised" ng-click="item.cancel()" ng-disabled="!item.isUploading">
<ng-md-icon icon="cancel" size="24"></ng-md-icon> Annuler
</md-button>
<md-button type="button" class="md-raised md-warn" ng-click="item.remove()">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon> Supprimer
</md-button>
</td>
</tr>
</tbody>
</table>
<div style='margin-top : 40px'>
<div>
Niveau de progression :
<md-progress-linear md-mode="determinate" value="{{uploader.progress}}"></md-progress-linear>
</div>
<md-button type="button" class="md-raised md-primary" ng-click="uploader.uploadAll()" ng-disabled="!uploader.getNotUploadedItems().length">
<ng-md-icon icon="file_upload" size="24"></ng-md-icon> Tout charger
</md-button>
<md-button type="button" class="md-raised" ng-click="uploader.cancelAll()" ng-disabled="!uploader.isUploading">
<ng-md-icon icon="cancel" size="24"></ng-md-icon> Tout annuler
</md-button>
<md-button type="button" class="md-raised md-warn" ng-click="uploader.clearQueue()" ng-disabled="!uploader.queue.length">
<ng-md-icon icon="remove_circle_outline" size="24"></ng-md-icon> Tout supprimer
</md-button>
</div>
</div>
</div>
</div>
</div>
</div>
</md-dialog-content>
<md-dialog-actions layout="row">
<span flex></span>
<md-button ng-click="answer('useful')">
Fermer
</md-button>
</md-dialog-actions>
</form>
</md-dialog>
</script>

View File

@ -8,6 +8,8 @@
</md-card-title-text>
</md-card-title>
<md-card-content>
<md-button ui-sref="administrationSpace">Log to administration</md-button>
<p class="p-home">
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...

View File

@ -13,37 +13,209 @@
</div>
<div layout="row" layout-align="center none">
<div ng-controller="AppCtrl" class="sample" layout="column" ng-cloak>
<md-content class="md-padding">
<md-tabs md-selected="selectedIndex" md-border-bottom md-autoselect>
<md-tab ng-repeat="tab in tabs"
ng-disabled="tab.disabled"
label="{{tab.title}}">
<div class="demo-tab tab{{$index%4}}" style="padding: 25px; text-align: center;">
<div ng-bind="tab.content"></div>
<br/>
<md-button class="md-primary md-raised" ng-click="removeTab( tab )" ng-disabled="tabs.length <= 1">Remove Tab</md-button>
<md-content flex="80">
<div ng-cloak>
<md-content>
<md-tabs md-dynamic-height md-border-bottom>
<md-tab label="périodes">
<md-content class="md-padding">
<div layout="row" layout-align="end">
<md-button class="md-raised" ng-click="exportBooklet()">Exporter le livret</md-button>
</div>
<md-card ng-repeat="period in periods">
<md-card-title>
<md-card-title-text>
<h2 class="md-display-1">Période {{period.number}}</h2>
</md-card-title-text>
</md-card-title>
<md-card-content>
<div layout="column" layout-fill>
<div flex class="green darken-2">
<h3 class="md-headline" style="padding-left : 10px">
Université
<md-button class="md-icon-button" ng-click="toggleAccordion(period.university.icon, 'false', $index)" aria-label="call">
<ng-md-icon icon="{{period.university.icon}}" style="fill: blue" size="24"></ng-md-icon>
</md-button>
</h3>
<md-content flex layout-padding ng-if="isOpenAccordion(period.university.icon)">
<p>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.</p><br/>
<md-input-container class="md-block">
<label>Commentaire de période</label>
<textarea ng-model="period.university.comment" md-maxlength="150" rows="5" md-select-on-focus></textarea>
</md-input-container>
<div layout="row" layout-align="end">
<md-button class="md-raised">Enregistrer</md-button>
</div>
</md-content>
</div>
<div flex class="green darken-3">
<h3 class="md-headline" style="padding-left : 10px">
Entreprise
<md-button class="md-icon-button" ng-click="toggleAccordion(period.company.icon, 'true', $index)" aria-label="call">
<ng-md-icon icon="{{period.company.icon}}" style="fill: blue" size="24"></ng-md-icon>
</md-button>
</h3>
<md-content flex layout-padding ng-if="isOpenAccordion(period.company.icon)">
<p>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.</p>
<md-input-container class="md-block">
<label>Commentaire de période</label>
<textarea ng-model="period.company.comment" md-maxlength="150" rows="5" md-select-on-focus></textarea>
</md-input-container>
<div layout="row" layout-align="end">
<md-button class="md-raised">Enregistrer</md-button>
</div>
</md-content>
</div>
</div>
</md-card-content>
</md-card>
</md-content>
</md-tab>
<md-tab label="données administratives">
<md-content class="md-padding">
<md-content layout-padding>
<form name="projectForm">
<md-card>
<md-card-title>
<md-card-title-text>
<h3 class="md-headline">Informations personnelles</h3>
</md-card-title-text>
</md-card-title>
<md-card-content>
<div layout="row">
<md-input-container flex="50">
<label>Prénom</label>
<input required name="studentFirstName" ng-model="studentFirstName">
</md-input-container>
<md-input-container flex="50">
<label>Nom</label>
<input required name="studentLastName" ng-model="studentLastName">
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50">
<label>Type de contrat / d'engagement</label>
<md-select name="type" ng-model="contractType" required>
<md-option value="Apprentissage">Apprentissage</md-option>
<md-option value="Professionnalisation">Professionnalisation</md-option>
<md-option value="Stage">Stage</md-option>
</md-select>
</md-input-container>
<md-input-container flex="25">
<label>Début de contrat</label>
<md-datepicker ng-model="beginContractDate"></md-datepicker>
</md-input-container>
<md-input-container flex="25">
<label>Fin de contrat</label>
<md-datepicker ng-model="endContractDate"></md-datepicker>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50">
<label>Email</label>
<input required type="email" name="studentEmail" ng-model="studentEmail" />
</md-input-container>
<md-input-container flex="50">
<label>Téléphone</label>
<input required type="text" name="studentPhoneNumber" ng-model="studentPhoneNumber" />
</md-input-container>
</div>
</md-card-content>
</md-card>
<md-card>
<md-card-title>
<md-card-title-text>
<h3 class="md-headline">Tuteur pédagogique</h3>
</md-card-title-text>
</md-card-title>
<md-card-content>
<md-input-container class="md-block">
<label>Email</label>
<input required type="email" name="universityEmail" ng-model="universityEmail" />
</md-input-container>
</md-card-content>
</md-card>
<md-card>
<md-card-title>
<md-card-title-text>
<h3 class="md-headline">Tuteur entreprise</h3>
</md-card-title-text>
</md-card-title>
<md-card-content>
<div layout="row">
<md-input-container flex="50">
<label>Prénom</label>
<input required name="companyFirstName" ng-model="companyFirstName">
</md-input-container>
<md-input-container flex="50">
<label>Nom</label>
<input required name="companyLastName" ng-model="companyLastName">
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50">
<label>Nom de l'entreprise</label>
<input name="companyName" ng-model="companyName">
</md-input-container>
<md-input-container flex="50">
<label>Lieu de l'alternance</label>
<input name="companyAddress" ng-model="companyAddress">
</md-input-container>
</div>
<md-input-container class="md-block">
<label>Email</label>
<input required type="email" name="companyEmail" ng-model="companyEmail" />
</md-input-container>
</md-card-content>
</md-card>
<div layout="row" layout-align="end">
<md-button class="md-raised">Enregistrer</md-button>
</div>
</form>
</md-content>
</md-tab>
</md-tabs>
</md-content>
</div>
</md-tab>
</md-tabs>
</md-content>
<form ng-submit="addTab(tTitle,tContent)" layout="column" class="md-padding" style="padding-top: 0;">
<div layout="row" layout-sm="column">
<div flex style="position: relative;">
<h2 class="md-subhead" style="position: absolute; bottom: 0; left: 0; margin: 0; font-weight: 500; text-transform: uppercase; line-height: 35px; white-space: nowrap;">Add a new Tab:</h2>
</div>
<md-input-container>
<label for="label">Label</label>
<input type="text" id="label" ng-model="tTitle">
</md-input-container>
<md-input-container>
<label for="content">Content</label>
<input type="text" id="content" ng-model="tContent">
</md-input-container>
<md-button class="add-tab md-primary md-raised" ng-disabled="!tTitle || !tContent" type="submit" style="margin-right: 0;">Add Tab</md-button>
</div>
</form>
</div>
</div>
</md-content>
</div>

View File

@ -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"

View File

@ -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',