Commit f257d092 by Pablo Molina

Solved #428, añadida categoría libre.

- Añadida comprobación de atributos de estudiante y picto válidos
- Actualizados modelos MetaStu y MetaPicto
- Actualizado el test CAJA
- Actualizado el log de la política isSupervisorOfStudent
- Actualizado el log de StudentController.add_picto
- Añadido log para la creación/actualización del modelo StuPicto
- Añadidos parámetros extra para el test POST /stu/stu_id/picto
parent ff51658d
...@@ -51,18 +51,67 @@ INSERT INTO `meta_stu` (`id`, `name`, `default`, `domain`, `description`) VALUES ...@@ -51,18 +51,67 @@ INSERT INTO `meta_stu` (`id`, `name`, `default`, `domain`, `description`) VALUES
(10, 'tts_options', '{"phrase": false}', 'phrase', 'Setup if phrase is spoken'), (10, 'tts_options', '{"phrase": false}', 'phrase', 'Setup if phrase is spoken'),
(11, 'picto_background', '"#0000ff"', 'RGB colors', 'Picto background color'), (11, 'picto_background', '"#0000ff"', 'RGB colors', 'Picto background color'),
(12, 'phrase_background', '"#ff0000"', 'RGB colors', 'Phrase background color'), (12, 'phrase_background', '"#ff0000"', 'RGB colors', 'Phrase background color'),
(13, 'legendsize', '"10"', '10-90', 'Size of legend in percentage '); (13, 'legendsize', '"10"', '10-90', 'Size of legend in percentage '),
(14, 'show_free_category', 'false', 'false,true', 'Show the free category to the student instead of the category grid');
-- --
-- Picto meta-properties -- Picto meta-properties
-- --
INSERT INTO `meta_picto` (`id`, `name`, `default`, `domain`, `description`) VALUES INSERT INTO `meta_picto` (
(1,'id_cat',NULL, NULL,'pictogram category, if any'), `id`,
(2,'coord_x',NULL, '1,2,3,4,5','picto screen row'), `name`,
(3,'coord_y',NULL, '1,2,3,4,5,6,7,8,9,10','picto screen column'), `default`,
(4,'status','"invisible"','enabled, disabled, invisible','enabled: the user can select the picto; disabled: the user see the picto but it is not selectable; invisible: neither selectable nor usable'), `domain`,
(5,'highlight','false','false, true','Highlight the pictogram in the PCB'), `description`
(6, 'color',NULL,'000000-FFFFFF','Picto background color'); ) VALUES (
1,
'id_cat',
NULL,
NULL,
'pictogram category, if any'
), (
2,
'coord_x',
NULL,
'1,2,3,4,5',
'picto screen row'
), (
3,
'coord_y',
NULL,
'1,2,3,4,5,6,7,8,9,10',
'picto screen column'
), (
4,
'status',
'invisible',
'enabled,disabled,invisible',
'enabled: the user can select the picto; disabled: the user see the picto but it is not selectable; invisible: neither selectable nor usable'
), (
5,
'highlight',
'false',
'false,true',
'Highlight the pictogram in the PCB'
), (
6,
'color',
NULL,
'000000,FFFFFF',
'Picto background color'
), (
7,
'free_category_coord_x',
NULL,
'1,2,3,4,5',
'Picto screen row when only a free category is being shown'
), (
8,
'free_category_coord_y',
NULL,
'1,2,3,4,5,6,7,8,9,10',
'Picto screen column when only a free category is being shown'
);
-- --
-- Volcado de datos para la tabla `source` -- Volcado de datos para la tabla `source`
......
...@@ -335,6 +335,14 @@ INSERT INTO `student` ( ...@@ -335,6 +335,14 @@ INSERT INTO `student` (
-- CAJA student-supervisor -- CAJA student-supervisor
-- --
INSERT INTO `stu_sup` (
id_stu,
id_sup
) VALUES (
(SELECT id FROM student WHERE username='aaa0002'),
(SELECT id FROM supervisor WHERE email='dofer@ujaen.es')
);
INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Fernandito' and B.name='Fernando'; INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Fernandito' and B.name='Fernando';
INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Fernandita' and B.name='Fernando'; INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Fernandita' and B.name='Fernando';
INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Arturito' and B.name='Fernando'; INSERT INTO `stu_sup`(id_stu,id_sup) SELECT DISTINCT A.id, B.id FROM student A, supervisor B WHERE A.name='Arturito' and B.name='Fernando';
......
...@@ -819,6 +819,8 @@ module.exports = { ...@@ -819,6 +819,8 @@ module.exports = {
}) })
.then(function (stuPicto) { .then(function (stuPicto) {
if (!stuPicto) { if (!stuPicto) {
sails.log.error(`Can't create StuPicto for picto ${params.id_picto} \
and student ${params.id_stu}`);
throw new Error('stupicto not added'); throw new Error('stupicto not added');
} }
...@@ -831,12 +833,16 @@ module.exports = { ...@@ -831,12 +833,16 @@ module.exports = {
}); });
}) })
.catch(function () { .catch(function () {
sails.log.debug(`Trying to find an existing StuPicto for picto ${params.id_picto} \
and student ${params.id_stu}`);
StuPicto.findOne({ StuPicto.findOne({
student: params.id_stu, student: params.id_stu,
picto: params.id_picto picto: params.id_picto
}) })
.then((stuPicto) => { .then((stuPicto) => {
if (!stuPicto) { if (!stuPicto) {
sails.log.error(`Can't create or find StuPicto for picto ${params.id_picto} \
and student ${params.id_stu}. Check if both picto and student exist and check`);
res.badRequest(); res.badRequest();
} else { } else {
res.ok({ res.ok({
...@@ -849,6 +855,8 @@ module.exports = { ...@@ -849,6 +855,8 @@ module.exports = {
} }
}) })
.catch(() => { .catch(() => {
sails.log.error(`Server error finding StuPicto for picto ${params.id_picto} \
and student ${params.id_stu}`);
res.serverError(); res.serverError();
}); });
}); });
......
/* global sails, MetaPicto */
/** /**
* MetaPicto.js * MetaPicto.js
* *
* @description :: Represents possible attributes for a Picto * @description :: Represents possible attributes for a Picto
* @docs :: http://sailsjs.org/#!documentation/models * @docs :: http://sailsjs.org/#!documentation/models
*/ */
module.exports = { module.exports = {
tableName : 'meta_picto', tableName: 'meta_picto',
migrate : 'safe', migrate: 'safe',
schema : true, schema: true,
autoPK : false, autoPK: false,
autoCreatedAt : false, autoCreatedAt: false,
autoUpdatedAt : false, autoUpdatedAt: false,
attributes: { attributes: {
id: { id: {
type: "integer", type: 'integer',
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
unique: true unique: true
}, },
name: { name: {
required: true, required: true,
type: "string", type: 'string',
size: 40 size: 40
}, },
default_val: { default_val: {
...@@ -31,9 +32,54 @@ module.exports = { ...@@ -31,9 +32,54 @@ module.exports = {
size: 40, size: 40,
required: false, required: false,
}, },
domain: {
columnName: 'domain',
type: 'string',
size: 250
},
description: { description: {
type: "string", type: 'string',
size: 1024 size: 1024
} }
},
/**
* For a given attribute object this function returns a promise which receives a valid result
* which can be stored safely in the database.
* It also adds the necesary default attributes if they are not present in the original object.
* This method can be used as class method (does not require a MetaPicto instance).
* @param {Object} attributes StuPicto attributes object
* @return {Object} StuPicto valid attributes object
*/
getValidAttributes(attributes) {
return MetaPicto.find().then(function (metaAttributes) {
var attributeValue;
var attributeDomains = {};
var validAttributes = {};
metaAttributes.forEach((metaAttribute) => {
validAttributes[metaAttribute.name] = metaAttribute.default_val;
if (typeof metaAttribute.domain === 'string') {
attributeDomains[metaAttribute.name] = metaAttribute.domain.split(',');
} else {
attributeDomains[metaAttribute.name] = true;
}
});
Object.keys(attributes).forEach((attributeKey) => {
attributeValue = attributes[attributeKey];
if (attributeDomains[attributeKey] &&
(attributeDomains[attributeKey] === true ||
attributeDomains[attributeKey].indexOf(attributeValue) !== -1
)) {
validAttributes[attributeKey] = attributeValue;
} else {
sails.log.error(`MetaPicto: received invalid attribute \
${attributeKey} with value "${attributeValue}`);
}
});
return validAttributes;
});
} }
}; };
\ No newline at end of file
/** /* global sails, MetaStu */
* MetaStu.js
*
* @description :: TODO: You might write a short summary of how this model works and what it represents here.
* @docs :: http://sailsjs.org/#!documentation/models
*/
/**
* This model contains all the valid attributes that the student "attributes" property can
* store. It is used as testing entity.
* @type {Object}
*/
module.exports = { module.exports = {
tableName : 'meta_stu', tableName: 'meta_stu',
migrate : 'safe', migrate: 'safe',
schema : true, schema: true,
autoPK : false, autoPK: false,
autoCreatedAt : false, autoCreatedAt: false,
autoUpdatedAt : false, autoUpdatedAt: false,
attributes: { attributes: {
id: { id: {
type: "integer", type: 'integer',
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
unique: true unique: true
}, },
name: { name: {
required: true, required: true,
type: "string", type: 'string',
size: 50 size: 50
}, },
default_val: { default_val: {
...@@ -32,15 +32,48 @@ module.exports = { ...@@ -32,15 +32,48 @@ module.exports = {
required: false required: false
}, },
domain: { domain: {
type: "string", type: 'string',
size: 250, size: 250,
required: false required: false
}, },
description: { description: {
type: "string", type: 'string',
size: 120, size: 120,
required: false required: false
} }
},
/**
* For a given attribute object this function returns a promise which receives a valid result
* which can be stored safely in the database.
* It also adds the necesary default attributes if they are not present in the original object.
* This method can be used as class method (does not require a MetaStu instance).
* @param {Object} attributes Student attributes object
* @return {Object} Student valid attributes object
*/
getValidAttributes(attributes) {
return MetaStu.find().then(function (metaAttributes) {
var attributeValue;
var attributeDomains = {};
var validAttributes = {};
metaAttributes.forEach((metaAttribute) => {
validAttributes[metaAttribute.name] = metaAttribute.default_val;
attributeDomains[metaAttribute.name] = metaAttribute.domain.split(',');
});
Object.keys(attributes).forEach((attributeKey) => {
attributeValue = attributes[attributeKey];
if (attributeDomains[attributeKey] &&
attributeDomains[attributeKey].indexOf(attributeValue) !== -1) {
validAttributes[attributeKey] = attributeValue;
} else {
sails.log.error(`Received invalid attribute \
${attributeKey} with value "${attributeValue}`);
}
});
return validAttributes;
});
} }
}; };
/* global sails, MetaPicto */
/** /**
* StuPicto.js * StuPicto model represents the collection of pictos for a student,
* * with the properties associated to it
* @description :: This model represents the collection of pictos for a student,
* with the properties associated to it
* @docs :: http://sailsjs.org/#!documentation/models
*/ */
module.exports = { module.exports = {
tableName : 'stu_picto', tableName: 'stu_picto',
migrate : 'safe', migrate: 'safe',
schema : true, schema: true,
autoPK : false, autoPK: false,
autoCreatedAt : false, autoCreatedAt: false,
autoUpdatedAt : false, autoUpdatedAt: false,
attributes: { attributes: {
id: { id: {
type: "integer", type: 'integer',
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
unique: true unique: true
}, },
student: { // FK de Student. 1 a N student: { // FK de Student. 1 a N
columnName: "id_stu", columnName: 'id_stu',
required: true, required: true,
type: "integer", type: 'integer',
model: "Student" model: 'Student'
}, },
picto: { // FK de Picto. 1 a N picto: { // FK de Picto. 1 a N
columnName: "id_pic", columnName: 'id_pic',
required: true, required: true,
type: "integer", type: 'integer',
model: "Picto" model: 'Picto'
}, },
attributes: { //JSON with attributes defined in meta_picto attributes: { // JSON with attributes defined in meta_picto
columnName: "attributes", columnName: 'attributes',
required: true, required: true,
type: "json" type: 'json'
} }
}, },
// /**
// Model hook for storing default picto configuration adding a new picto * Checks the given attributes before creating a new StuPicto
// * @param {Object} attrs All StuPicto properties to be stored
beforeCreate: function(attrs, next) { * @param {Function} next Function to be executed when the check process
* has been completed (an error object will be passed
MetaPicto.find().exec(function(err, metaPicto) { * to the function if necesary)
*/
if (err || !metaPicto || metaPicto.length == 0) beforeCreate: function (attrs, next) {
return next(err); sails.log.debug('Checking attributes for new StuPicto');
/* Default Configuration: MetaPicto.getValidAttributes(attrs.attributes).then((validAttributes) => {
var configuration = { attrs.attributes = validAttributes;
"id_cat" : null, next();
"coord_x" : null, }).catch((error) => {
"coord_y" : null, sails.log.error(`Error while checking attributes for new StuPicto: ${error}`);
"status": "enabled" next(error || new Error());
}; });
*/ },
var configuration = [];
// The default configuration is built by meta_picto fields and its default values
for (var j = 0; j< metaPicto.length; j++) {
configuration.push('"' + metaPicto[j].name + '":' + metaPicto[j].default_val);
}
// If the picto is from picto_core_cat, it has defined the coords
if(attrs.attributes.coord_x !== undefined && attrs.attributes.coord_y !== undefined){
var previous_coord_x = attrs.attributes.coord_x;
var previous_coord_y = attrs.attributes.coord_y;
}
if(attrs.attributes.id_cat !== undefined)
var previous_id_cat = attrs.attributes.id_cat;
if(attrs.attributes.status !== undefined)
var previous_status = attrs.attributes.status;
if(attrs.attributes.color !== undefined)
var previous_color = attrs.attributes.color;
attrs.attributes = JSON.parse("{" + configuration.toString() + "}");
// And we store them (If the picto is from picto_core, it has defined the coords)
if(previous_coord_x !== undefined && previous_coord_y !== undefined){
attrs.attributes.coord_x = previous_coord_x;
attrs.attributes.coord_y = previous_coord_y;
}
if(previous_id_cat !== undefined)
attrs.attributes.id_cat = previous_id_cat;
if(previous_status !== undefined)
attrs.attributes.status = previous_status;
if(previous_color !== undefined)
attrs.attributes.color = previous_color;
/**
* Checks the given properties before updating a new Student
* @param {Object} attrs All student properties to be stored
* @param {Function} next Function to be executed when the check process
* has been completed (an error object will be passed
* to the function if necesary)
*/
beforeUpdate: function (attrs, next) {
sails.log.debug(`Checking attributes for StuPicto ${attrs.id}`);
delete attrs.username;
MetaPicto.getValidAttributes(attrs.attributes).then((validAttributes) => {
attrs.attributes = validAttributes;
next(); next();
}).catch((error) => {
sails.log.error(`Error while checking attributes for StuPicto ${attrs.id}: ${error}`);
next(error || new Error());
}); });
} }
};
}
/* global sails, MetaStu */ /* global sails, MetaStu */
const bcrypt = require('bcrypt-nodejs') const bcrypt = require('bcrypt-nodejs');
/** /**
* student.js * student.js
...@@ -10,65 +10,65 @@ const bcrypt = require('bcrypt-nodejs') ...@@ -10,65 +10,65 @@ const bcrypt = require('bcrypt-nodejs')
*/ */
module.exports = { module.exports = {
tableName : 'student', tableName: 'student',
migrate : 'safe', migrate: 'safe',
schema : true, schema: true,
autoPK : false, autoPK: false,
autoCreatedAt : false, autoCreatedAt: false,
autoUpdatedAt : false, autoUpdatedAt: false,
attributes: { attributes: {
id: { id: {
type: "integer", type: 'integer',
autoIncrement: true, autoIncrement: true,
primaryKey: true, primaryKey: true,
unique: true unique: true
}, },
username: { username: {
required: true, required: true,
type: "string", type: 'string',
size: 40 size: 40
}, },
password: { password: {
required: true, required: true,
type: "string", type: 'string',
size: 40 size: 40
}, },
name: { name: {
required: true, required: true,
type: "string", type: 'string',
size: 40 size: 40
}, },
surname: { surname: {
required: true, required: true,
type: "string", type: 'string',
size: 60 size: 60
}, },
birthdate: { birthdate: {
required: true, required: true,
type: "date" type: 'date'
}, },
gender:{ gender: {
required: true, required: true,
type: "string", type: 'string',
size: 1 size: 1
}, },
country:{ country: {
type: "string", type: 'string',
size: 2 size: 2
}, },
pic:{ pic: {
type: "string", type: 'string',
size: 255 size: 255
}, },
notes: { notes: {
type: "string", type: 'string',
size: 1024 size: 1024
}, },
lang: { lang: {
columnName: "lang", columnName: 'lang',
required: true, required: true,
type: "string", type: 'string',
size: 2 size: 2
}, },
attributes: { attributes: {
...@@ -84,26 +84,26 @@ module.exports = { ...@@ -84,26 +84,26 @@ module.exports = {
}, },
// Relación con StuSup // Relación con StuSup
stuSup: { stuSup: {
collection: "StuSup", collection: 'StuSup',
via: "student" via: 'student'
}, },
// Relación con Method. [1 Student to N Method] // Relación con Method. [1 Student to N Method]
methods: { methods: {
collection: "Method", collection: 'Method',
via: "student" via: 'student'
}, },
// Relación con StuPicto. [1 Student to N StuPicto] // Relación con StuPicto. [1 Student to N StuPicto]
stuPicto: { stuPicto: {
collection: "StuPicto", collection: 'StuPicto',
via: "student" via: 'student'
}, },
},
toJSON: function () { toJSON: function () {
var student = this.toObject(); var student = this.toObject();
student.pic = sails.config.pictogram.urls.getStudentAvatarUrl(student.pic); student.pic = sails.config.pictogram.urls.getStudentAvatarUrl(student.pic);
delete student.password; delete student.password;
return student; return student;
},
}, },
/** /**
...@@ -114,34 +114,32 @@ module.exports = { ...@@ -114,34 +114,32 @@ module.exports = {
* to the function if necesary) * to the function if necesary)
*/ */
beforeCreate: function (attrs, next) { beforeCreate: function (attrs, next) {
MetaStu.find().then(function (metaAttributes) { MetaStu.getValidAttributes(attrs.attributes).then((validAttributes) => {
var attributeValue; attrs.attributes = validAttributes;
var attributeDomains = {};
var validAttributes = {};
metaAttributes.forEach((metaAttribute) => {
validAttributes[metaAttribute.name] = metaAttribute.default_val;
attributeDomains[metaAttribute.name] = metaAttribute.domain.split(',');
});
attrs.attributes.keys().forEach((attributeKey) => {
attributeValue = attrs.attributes[attributeKey];
if (attributeDomains[attributeKey] &&
attributeDomains[attributeKey].indexOf(attributeValue) !== -1) {
validAttributes[attributeKey] = attributeValue;
} else {
sails.log.error(`Received invalid attribute \
${attributeKey} with value "${attributeValue}`);
}
});
attrs.password = bcrypt.hashSync(attrs.password, bcrypt.genSaltSync()); attrs.password = bcrypt.hashSync(attrs.password, bcrypt.genSaltSync());
next();
}).catch((error) => { }).catch((error) => {
if (error) { next(error || new Error());
next(error); });
} else { },
next(new Error());
/**
* Checks the given properties before updating a new Student
* @param {Object} attrs All student properties to be stored
* @param {Function} next Function to be executed when the check process
* has been completed (an error object will be passed
* to the function if necesary)
*/
beforeUpdate: function (attrs, next) {
delete attrs.username;
MetaStu.getValidAttributes(attrs.attributes).then((validAttributes) => {
attrs.attributes = validAttributes;
if (attrs.password) {
attrs.password = bcrypt.hashSync(attrs.password, bcrypt.genSaltSync());
} }
next();
}).catch((error) => {
next(error || new Error());
}); });
}, },
...@@ -327,31 +325,5 @@ module.exports = { ...@@ -327,31 +325,5 @@ module.exports = {
return cb(); return cb();
}); });
}); });
}, }
//
// Model hook for storing encrypted password when updating an account
// and to check that the user new username is not already registered
//
beforeUpdate: function(attrs, next) {
// remove username as it shouldn't be changed
delete attrs.username;
//
// Encrypt password before insertion
//
var bcrypt = require('bcrypt-nodejs');
bcrypt.genSalt(10, function(err, salt) {
if (err || !attrs.password)
return next(err);
bcrypt.hash(attrs.password, salt, null, function(err, hash) {
if (err)
return next(err);
attrs.password = hash;
next();
});
});
},
}; };
module.exports = function isSupervisorOfStudent (req, res, next) { /* global sails, Student */
if (!req.params.id_stu) module.exports = function isSupervisorOfStudent(req, res, next) {
return res.json(401, {error: 'Access denied'}); const supervisorId = req.token.id;
const studentId = req.params.id_stu;
// Get list of supervisors for the student if (!studentId || !supervisorId) {
Student.supervisors(req.params.id_stu, function(err, sups) { sails.log.error('This request needs an id_stu parameter and a authenticated supervisor');
if (err || !sups || sups.length == 0) res.json(401, { error: 'Access denied' });
return res.json(401, {error: 'Access denied'}); } else {
Student.supervisors(studentId, function (err, sups) {
const studentSupervisorsIds = sups.map((studentSupervisor) => studentSupervisor.id);
// if supervisor is not in the list of supervisors if (err || studentSupervisorsIds.length === 0) {
if (sups.map(function(e) {return e.id}).indexOf(req.token.id) < 0) sails.log.error(`Student ${studentId} has no supervisor assigned`);
return res.json(401, {error: 'Access denied'}); res.json(401, { error: 'Access denied' });
} else if (studentSupervisorsIds.indexOf(supervisorId) < 0) {
// Finally, if the user has a clean record, we'll call the `next()` function sails.log.error(`Supervisor ${supervisorId} is not assigned to Student ${studentId}`);
// to let them through to the next policy or our controller sails.log.debug(`Student supervisors: ${studentSupervisorsIds}`);
next(); res.json(401, { error: 'Access denied' });
}); } else {
sails.log.debug(`Supervisor ${supervisorId} is assigned to Student ${studentId}`);
next();
}; }
\ No newline at end of file });
}
};
const path = require('path');
describe('Student API', function () { describe('Student API', function () {
it('GET /stu/:id_stu', function (done) { it('GET /stu/:id_stu', function (done) {
const studentAgentData = Object.assign({}, studentAgent.data); const studentAgentData = Object.assign({}, studentAgent.data);
...@@ -275,8 +277,6 @@ describe('Student API', function () { ...@@ -275,8 +277,6 @@ describe('Student API', function () {
}); });
it('POST /stu'); it('POST /stu');
it('POST /stu/upload', function (done) { it('POST /stu/upload', function (done) {
const path = require('path');
studentAgent studentAgent
.post('/stu/upload') .post('/stu/upload')
.field('id', studentAgent.data.id) .field('id', studentAgent.data.id)
...@@ -310,7 +310,9 @@ describe('Student API', function () { ...@@ -310,7 +310,9 @@ describe('Student API', function () {
id_cat: 12345, id_cat: 12345,
color: null, color: null,
highlight: false, highlight: false,
status: 'invisible' status: 'invisible',
free_category_coord_x: null,
free_category_coord_y: null
} }
}) })
.end(done); .end(done);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment