Merge branch 'dev' into 2517-clientBalanceCompensaciones
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
Javi Gallego 2020-12-22 07:42:28 +01:00
commit 36bace2a94
177 changed files with 1162 additions and 387 deletions

6
.gitignore vendored
View File

@ -1,11 +1,7 @@
coverage coverage
node_modules node_modules
dist dist
e2e/dms/*/ storage
!e2e/dms/c4c
!e2e/dms/c81
!e2e/dms/ecc
!e2e/dms/a87
npm-debug.log npm-debug.log
.eslintcache .eslintcache
datasources.*.json datasources.*.json

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('updateFile', { Self.remoteMethodCtx('updateFile', {
@ -84,66 +85,46 @@ module.exports = Self => {
}; };
async function uploadNewFile(ctx, dms, myOptions) { async function uploadNewFile(ctx, dms, myOptions) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models; const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {}; const fileOptions = {};
const tempContainer = await TempContainer.container('dms');
const tempContainer = await getContainer('temp'); const makeUpload = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const makeUpload = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const keys = Object.values(makeUpload.files); const keys = Object.values(makeUpload.files);
const files = keys.map(file => file[0]); const files = keys.map(file => file[0]);
const file = files[0]; const uploadedFile = files[0];
if (file) { if (uploadedFile) {
const oldExtension = storageConnector.getFileExtension(dms.file); const oldExtension = DmsContainer.getFileExtension(dms.file);
const newExtension = storageConnector.getFileExtension(file.name); const newExtension = DmsContainer.getFileExtension(uploadedFile.name);
const fileName = `${dms.id}.${newExtension}`; const fileName = `${dms.id}.${newExtension}`;
try { try {
if (oldExtension != newExtension) { if (oldExtension != newExtension) {
const pathHash = storageConnector.getPathHash(dms.id); const pathHash = DmsContainer.getHash(dms.id);
await models.Container.removeFile(pathHash, dms.file); await DmsContainer.removeFile(pathHash, dms.file);
} }
} catch (err) {} } catch (err) {}
const updatedDms = await dms.updateAttributes({ const updatedDms = await dms.updateAttributes({
contentType: file.type, contentType: uploadedFile.type,
file: fileName file: fileName
}, myOptions); }, myOptions);
const pathHash = storageConnector.getPathHash(updatedDms.id); const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
const container = await getContainer(pathHash); const srcFile = path.join(file.client.root, file.container, file.name);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`; const pathHash = DmsContainer.getHash(updatedDms.id);
const destinationPath = `${container.client.root}/${pathHash}/${updatedDms.file}`; const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, updatedDms.file);
fs.rename(originPath, destinationPath); await fs.move(srcFile, dstFile, {
overwrite: true
});
return updatedDms; return updatedDms;
} }
} }
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
async function getContainer(name) {
const models = Self.app.models;
let container;
try {
container = await models.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
}; };

View File

@ -1,5 +1,6 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('uploadFile', { Self.remoteMethodCtx('uploadFile', {
@ -46,8 +47,9 @@ module.exports = Self => {
}); });
Self.uploadFile = async(ctx, options) => { Self.uploadFile = async(ctx, options) => {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models; const models = Self.app.models;
const TempContainer = models.TempContainer;
const DmsContainer = models.DmsContainer;
const fileOptions = {}; const fileOptions = {};
const args = ctx.args; const args = ctx.args;
@ -62,28 +64,33 @@ module.exports = Self => {
myOptions.transaction = tx; myOptions.transaction = tx;
} }
let srcFile;
try { try {
const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions); const hasWriteRole = await models.DmsType.hasWriteRole(ctx, args.dmsTypeId, myOptions);
if (!hasWriteRole) if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
// Upload file to temporary path // Upload file to temporary path
const tempContainer = await getContainer('temp'); const tempContainer = await TempContainer.container('dms');
const uploaded = await models.Container.upload(tempContainer.name, ctx.req, ctx.result, fileOptions); const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const files = Object.values(uploaded.files).map(file => { const files = Object.values(uploaded.files).map(file => {
return file[0]; return file[0];
}); });
const addedDms = []; const addedDms = [];
for (const file of files) { for (const uploadedFile of files) {
const newDms = await createDms(ctx, file, myOptions); const newDms = await createDms(ctx, uploadedFile, myOptions);
const pathHash = storageConnector.getPathHash(newDms.id); const pathHash = DmsContainer.getHash(newDms.id);
const container = await getContainer(pathHash);
const originPath = `${tempContainer.client.root}/${tempContainer.name}/${file.name}`; const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
const destinationPath = `${container.client.root}/${pathHash}/${newDms.file}`; srcFile = path.join(file.client.root, file.container, file.name);
await fs.rename(originPath, destinationPath); const dmsContainer = await DmsContainer.container(pathHash);
const dstFile = path.join(dmsContainer.client.root, pathHash, newDms.file);
await fs.move(srcFile, dstFile, {
overwrite: true
});
addedDms.push(newDms); addedDms.push(newDms);
} }
@ -92,13 +99,16 @@ module.exports = Self => {
return addedDms; return addedDms;
} catch (e) { } catch (e) {
if (tx) await tx.rollback(); if (tx) await tx.rollback();
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e; throw e;
} }
}; };
async function createDms(ctx, file, myOptions) { async function createDms(ctx, file, myOptions) {
const models = Self.app.models; const models = Self.app.models;
const storageConnector = Self.app.dataSources.storage.connector;
const myUserId = ctx.req.accessToken.userId; const myUserId = ctx.req.accessToken.userId;
const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions); const myWorker = await models.Worker.findOne({where: {userFk: myUserId}}, myOptions);
const args = ctx.args; const args = ctx.args;
@ -115,33 +125,9 @@ module.exports = Self => {
}, myOptions); }, myOptions);
let fileName = file.name; let fileName = file.name;
const extension = storageConnector.getFileExtension(fileName); const extension = models.DmsContainer.getFileExtension(fileName);
fileName = `${newDms.id}.${extension}`; fileName = `${newDms.id}.${extension}`;
return newDms.updateAttribute('file', fileName, myOptions); return newDms.updateAttribute('file', fileName, myOptions);
} }
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
async function getContainer(name) {
const models = Self.app.models;
let container;
try {
container = await models.Container.getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models.Container.createContainer({
name: name
});
} else throw err;
}
return container;
}
}; };

View File

@ -1,8 +1,9 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('path');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('download', { Self.remoteMethodCtx('download', {
description: 'Get the user image', description: 'Get the user image',
accessType: 'READ', accessType: 'READ',
accepts: [ accepts: [
@ -49,15 +50,9 @@ module.exports = Self => {
} }
}); });
Self.download = async function(collection, size, id) { Self.download = async function(ctx, collection, size, id) {
const models = Self.app.models; const models = Self.app.models;
const filter = { const filter = {where: {name: collection}};
where: {
name: collection},
include: {
relation: 'readRole'
}
};
const imageCollection = await models.ImageCollection.findOne(filter); const imageCollection = await models.ImageCollection.findOne(filter);
const entity = await models[imageCollection.model].findById(id, { const entity = await models[imageCollection.model].findById(id, {
fields: ['id', imageCollection.property] fields: ['id', imageCollection.property]
@ -69,28 +64,23 @@ module.exports = Self => {
if (!image) return false; if (!image) return false;
const imageRole = imageCollection.readRole().name; const hasReadRole = models.ImageCollection.hasReadRole(ctx, collection);
const hasRole = await models.Account.hasRole(id, imageRole); if (!hasReadRole)
if (!hasRole)
throw new UserError(`You don't have enough privileges`); throw new UserError(`You don't have enough privileges`);
let file; const container = await models.ImageContainer.getContainer(collection);
let env = process.env.NODE_ENV; const rootPath = container.client.root;
if (env && env != 'development') { const fileSrc = path.join(rootPath, collection, size);
file = { const file = {
path: `/var/lib/salix/image/${collection}/${size}/${image.name}.png`, path: `${fileSrc}/${image.name}.png`,
contentType: 'image/png', contentType: 'image/png',
name: `${image.name}.png` name: `${image.name}.png`
}; };
} else {
file = { if (!fs.existsSync(file.path)) return [];
path: `${process.cwd()}/storage/image/${collection}/${size}/${image.name}.png`,
contentType: 'image/png',
name: `${image.name}.png`
};
}
await fs.access(file.path); await fs.access(file.path);
let stream = fs.createReadStream(file.path); const stream = fs.createReadStream(file.path);
return [stream, file.contentType, `filename="${file.name}"`]; return [stream, file.contentType, `filename="${file.name}"`];
}; };
}; };

View File

@ -3,10 +3,12 @@ const app = require('vn-loopback/server/server');
describe('image download()', () => { describe('image download()', () => {
const collection = 'user'; const collection = 'user';
const size = '160x160'; const size = '160x160';
const employeeId = 1;
const ctx = {req: {accessToken: {userId: employeeId}}};
it('should return the image content-type of the user', async() => { it('should return the image content-type of the user', async() => {
const userId = 9; const userId = 9;
const image = await app.models.Image.download(collection, size, userId); const image = await app.models.Image.download(ctx, collection, size, userId);
const contentType = image[1]; const contentType = image[1];
expect(contentType).toEqual('image/png'); expect(contentType).toEqual('image/png');
@ -14,7 +16,7 @@ describe('image download()', () => {
it(`should return false if the user doesn't have image`, async() => { it(`should return false if the user doesn't have image`, async() => {
const userId = 110; const userId = 110;
const image = await app.models.Image.download(collection, size, userId); const image = await app.models.Image.download(ctx, collection, size, userId);
expect(image).toBeFalse(); expect(image).toBeFalse();
}); });

View File

@ -0,0 +1,129 @@
const app = require('vn-loopback/server/server');
describe('image upload()', () => {
describe('as buyer', () => {
const buyerId = 35;
const workerId = 106;
const itemId = 4;
it('should try to upload a file for the collection "catalog" and throw a privileges error', async() => {
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: workerId,
collection: 'user'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
it('should call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: buyerId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as marketing', () => {
const marketingId = 51;
const workerId = 106;
const itemId = 4;
it('should be able to call to the TempContainer upload method for the collection "user"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: workerId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should be able to call to the TempContainer upload method for the collection "catalog"', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: marketingId}},
args: {
id: itemId,
collection: 'catalog'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
});
describe('as hhrr', () => {
const hhrrId = 37;
const workerId = 106;
const itemId = 4;
it('should upload a file for the collection "user" and call to the TempContainer upload method', async() => {
const containerModel = app.models.TempContainer;
spyOn(containerModel, 'upload');
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: itemId,
collection: 'user'
}
};
try {
await app.models.Image.upload(ctx);
} catch (err) { }
expect(containerModel.upload).toHaveBeenCalled();
});
it('should try to upload a file for the collection "catalog" and throw a privilege error', async() => {
const ctx = {req: {accessToken: {userId: hhrrId}},
args: {
id: workerId,
collection: 'catalog'
}
};
let error;
try {
await app.models.Image.upload(ctx);
} catch (err) {
error = err;
}
expect(error.message).toEqual(`You don't have enough privileges`);
});
});
});

View File

@ -0,0 +1,70 @@
const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
const path = require('path');
module.exports = Self => {
Self.remoteMethodCtx('upload', {
description: 'Uploads a file and inserts into dms model',
accessType: 'WRITE',
accepts: [
{
arg: 'id',
type: 'Number',
description: 'The entity id',
required: true
},
{
arg: 'collection',
type: 'string',
description: 'The collection name',
required: true
},
{
arg: 'fileName',
type: 'string',
description: 'The file name',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/upload`,
verb: 'POST'
}
});
Self.upload = async ctx => {
const models = Self.app.models;
const TempContainer = models.TempContainer;
const fileOptions = {};
const args = ctx.args;
let srcFile;
try {
const hasWriteRole = await models.ImageCollection.hasWriteRole(ctx, args.collection);
if (!hasWriteRole)
throw new UserError(`You don't have enough privileges`);
if (process.env.NODE_ENV == 'test')
throw new UserError(`You can't upload images on the test instance`);
// Upload file to temporary path
const tempContainer = await TempContainer.container(args.collection);
const uploaded = await TempContainer.upload(tempContainer.name, ctx.req, ctx.result, fileOptions);
const [uploadedFile] = Object.values(uploaded.files).map(file => {
return file[0];
});
const file = await TempContainer.getFile(tempContainer.name, uploadedFile.name);
srcFile = path.join(file.client.root, file.container, file.name);
await models.Image.registerImage(args.collection, srcFile, args.fileName, args.id);
} catch (e) {
if (fs.existsSync(srcFile))
await fs.unlink(srcFile);
throw e;
}
};
};

View File

@ -17,9 +17,6 @@
"Company": { "Company": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Container": {
"dataSource": "storage"
},
"Continent": { "Continent": {
"dataSource": "vn" "dataSource": "vn"
}, },
@ -35,6 +32,9 @@
"Delivery": { "Delivery": {
"dataSource": "vn" "dataSource": "vn"
}, },
"DmsContainer": {
"dataSource": "dmsStorage"
},
"Image": { "Image": {
"dataSource": "vn" "dataSource": "vn"
}, },
@ -44,12 +44,18 @@
"ImageCollectionSize": { "ImageCollectionSize": {
"dataSource": "vn" "dataSource": "vn"
}, },
"ImageContainer": {
"dataSource": "imageStorage"
},
"Language": { "Language": {
"dataSource": "vn" "dataSource": "vn"
}, },
"Province": { "Province": {
"dataSource": "vn" "dataSource": "vn"
}, },
"TempContainer": {
"dataSource": "tempStorage"
},
"UserConfig": { "UserConfig": {
"dataSource": "vn" "dataSource": "vn"
}, },

View File

@ -1,13 +0,0 @@
{
"name": "Container",
"base": "VnModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}

View File

@ -0,0 +1,10 @@
{
"name": "DmsContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -14,12 +14,12 @@ module.exports = Self => {
}; };
Self.getFile = async function(id) { Self.getFile = async function(id) {
const storageConnector = Self.app.dataSources.storage.connector;
const models = Self.app.models; const models = Self.app.models;
const DmsContainer = models.DmsContainer;
const dms = await Self.findById(id); const dms = await Self.findById(id);
const pathHash = storageConnector.getPathHash(dms.id); const pathHash = DmsContainer.getHash(dms.id);
try { try {
await models.Container.getFile(pathHash, dms.file); await DmsContainer.getFile(pathHash, dms.file);
} catch (e) { } catch (e) {
if (e.code != 'ENOENT') if (e.code != 'ENOENT')
throw e; throw e;
@ -30,7 +30,7 @@ module.exports = Self => {
throw error; throw error;
} }
const stream = models.Container.downloadStream(pathHash, dms.file); const stream = DmsContainer.downloadStream(pathHash, dms.file);
return [stream, dms.contentType, `filename="${dms.file}"`]; return [stream, dms.contentType, `filename="${dms.file}"`];
}; };

View File

@ -0,0 +1,64 @@
module.exports = Self => {
/**
* Checks if current user has
* read privileges over a collection
*
* @param {object} ctx - Request context
* @param {interger} name - Collection name
* @param {object} options - Query options
* @return {boolean} True for user with read privileges
*/
Self.hasReadRole = async(ctx, name, options) => {
const collection = await Self.findOne({where: {name}}, {
include: {
relation: 'readRole'
}
}, options);
return await hasRole(ctx, collection, options);
};
/**
* Checks if current user has
* write privileges over a collection
*
* @param {object} ctx - Request context
* @param {string} name - Collection name
* @param {object} options - Query options
* @return {boolean} True for user with write privileges
*/
Self.hasWriteRole = async(ctx, name, options) => {
const collection = await Self.findOne({
include: {
relation: 'writeRole'
},
where: {name}
}, options);
return await hasRole(ctx, collection, options);
};
/**
* Checks if current user has
* read or write privileges
* @param {Object} ctx - Context
* @param {Object} collection - Collection [read/write]
* @param {Object} options - Query options
*/
async function hasRole(ctx, collection, options) {
const models = Self.app.models;
const myUserId = ctx.req.accessToken.userId;
const readRole = collection.readRole() && collection.readRole().name;
const writeRole = collection.writeRole() && collection.writeRole().name;
const requiredRole = readRole || writeRole;
const hasRequiredRole = await models.Account.hasRole(myUserId, requiredRole, options);
const isRoot = await models.Account.hasRole(myUserId, 'root', options);
if (isRoot || hasRequiredRole)
return true;
return false;
}
};

View File

@ -48,7 +48,12 @@
"type": "belongsTo", "type": "belongsTo",
"model": "Role", "model": "Role",
"foreignKey": "readRoleFk" "foreignKey": "readRoleFk"
} },
"writeRole": {
"type": "belongsTo",
"model": "Role",
"foreignKey": "writeRoleFk"
}
}, },
"acls": [ "acls": [
{ {

View File

@ -0,0 +1,10 @@
{
"name": "ImageContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -4,12 +4,9 @@ const path = require('path');
module.exports = Self => { module.exports = Self => {
require('../methods/image/download')(Self); require('../methods/image/download')(Self);
require('../methods/image/upload')(Self);
Self.getPath = function() { Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
return '/var/lib/salix/image';
};
Self.registerImage = async(collectionName, file, srcFilePath) => {
const models = Self.app.models; const models = Self.app.models;
const tx = await Self.beginTransaction({}); const tx = await Self.beginTransaction({});
const myOptions = {transaction: tx}; const myOptions = {transaction: tx};
@ -33,13 +30,10 @@ module.exports = Self => {
} }
}, myOptions); }, myOptions);
const fileName = file.split('.')[0];
const rootPath = Self.getPath();
const data = { const data = {
name: fileName, name: fileName,
collectionFk: collectionName collectionFk: collectionName
}; };
const newImage = await Self.upsertWithWhere(data, { const newImage = await Self.upsertWithWhere(data, {
name: fileName, name: fileName,
collectionFk: collectionName, collectionFk: collectionName,
@ -47,7 +41,10 @@ module.exports = Self => {
}, myOptions); }, myOptions);
// Resizes and saves the image // Resizes and saves the image
const container = await models.ImageContainer.container(collectionName);
const rootPath = container.client.root;
const collectionDir = path.join(rootPath, collectionName); const collectionDir = path.join(rootPath, collectionName);
const file = `${fileName}.png`;
const dstDir = path.join(collectionDir, 'full'); const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file); const dstFile = path.join(dstDir, file);
@ -83,7 +80,7 @@ module.exports = Self => {
if (!model) if (!model)
throw new Error('Matching model not found'); throw new Error('Matching model not found');
const item = await model.findById(fileName, null, myOptions); const item = await model.findById(entityId, null, myOptions);
if (item) { if (item) {
await item.updateAttribute( await item.updateAttribute(
collection.property, collection.property,

View File

@ -0,0 +1,10 @@
{
"name": "TempContainer",
"base": "Container",
"acls": [{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}]
}

View File

@ -0,0 +1 @@
INSERT INTO salix.ACL (model, property, accessType, permission, principalType, principalId) VALUES ('Image', '*', 'WRITE', 'ALLOW', 'ROLE', 'employee')

View File

@ -0,0 +1,13 @@
INSERT INTO account.role (id, name, description)
VALUES
(74, 'userPhotos', 'Privilegios para subir fotos de usuario'),
(75, 'catalogPhotos', 'Privilegios para subir fotos del catálogo');
INSERT INTO account.roleInherit (role, inheritsFrom)
VALUES
(37, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'userPhotos')),
(51, (SELECT id FROM account.role WHERE name = 'catalogPhotos')),
(35, (SELECT id FROM account.role WHERE name = 'catalogPhotos'));
CALL account.role_sync();

View File

@ -0,0 +1,27 @@
ALTER TABLE `hedera`.`imageCollection`
ADD writeRoleFk INT UNSIGNED NULL DEFAULT 1;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_writeRoleFk
FOREIGN KEY (writeRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
ALTER TABLE `hedera`.`imageCollection` modify readRoleFk INT UNSIGNED default 1 null;
ALTER TABLE `hedera`.`imageCollection`
ADD CONSTRAINT role_id_readRoleFk
FOREIGN KEY (readRoleFk) REFERENCES account.role (id)
ON UPDATE CASCADE;
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'catalogPhotos'
)
WHERE t.name = 'catalog';
UPDATE hedera.imageCollection t SET t.writeRoleFk = (
SELECT id FROM `account`.`role` WHERE name = 'userPhotos'
)
WHERE t.name = 'user';
UPDATE hedera.imageCollection t SET t.writeRoleFk = 9
WHERE t.name IN ('link', 'news');

View File

@ -447,6 +447,25 @@ INSERT INTO `imageCollection` VALUES (1,'catalog','Artículo',3840,2160,'Item','
/*!40000 ALTER TABLE `imageCollection` ENABLE KEYS */; /*!40000 ALTER TABLE `imageCollection` ENABLE KEYS */;
UNLOCK TABLES; UNLOCK TABLES;
--
-- Dumping data for table `imageCollectionSize`
--
LOCK TABLES `imageCollectionSize` WRITE;
/*!40000 ALTER TABLE `imageCollectionSize` DISABLE KEYS */;
INSERT INTO `imageCollectionSize` (`id`, `collectionFk`, `width`, `height`, `crop`)
VALUES
(2, 1, 50, 50, 1),
(3, 1, 200, 200, 1),
(5, 5, 200, 200, 1),
(6, 1, 70, 70, 1),
(8, 5, 50, 50, 1),
(9, 1, 1600, 900, 0),
(13, 6, 160, 160, 1),
(14, 6, 520, 520, 1),
(15, 6, 1600, 1600, 1);
/*!40000 ALTER TABLE `imageCollectionSize` ENABLE KEYS */;
UNLOCK TABLES;
-- --
-- Dumping data for table `tpvError` -- Dumping data for table `tpvError`
-- --

View File

@ -30,7 +30,7 @@ UPDATE `account`.`role` SET id = 100 WHERE id = 0;
CALL `account`.`role_sync`; CALL `account`.`role_sync`;
INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`, `image`) INSERT INTO `account`.`user`(`id`,`name`, `nickname`, `password`,`role`,`active`,`email`, `lang`, `image`)
SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', 'e7723f0b24ff05b32ed09d95196f2f29' SELECT id, name, CONCAT(name, 'Nick'),MD5('nightmare'), id, 1, CONCAT(name, '@mydomain.com'), 'en', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd'
FROM `account`.`role` WHERE id <> 20 FROM `account`.`role` WHERE id <> 20
ORDER BY id; ORDER BY id;
@ -773,23 +773,23 @@ INSERT INTO `vn`.`intrastat`(`id`, `description`, `taxClassFk`, `taxCodeFk`)
INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`) INSERT INTO `vn`.`item`(`id`, `typeFk`, `size`, `inkFk`, `stems`, `originFk`, `description`, `producerFk`, `intrastatFk`, `isOnOffer`, `expenceFk`, `isBargain`, `comment`, `relevancy`, `image`, `taxClassFk`, `subName`, `minPrice`)
VALUES VALUES
(1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 67, 1, NULL, 0), (1, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '1', 1, NULL, 0),
(2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 66, 1, NULL, 0), (2, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '2', 1, NULL, 0),
(3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 65, 1, NULL, 0), (3, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '3', 1, NULL, 0),
(4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, 69, 2, NULL, 0), (4, 1, 60, 'YEL', 1, 1, 'Increases block', 1, 05080000, 1, 4751000000, 0, NULL, 0, '4', 2, NULL, 0),
(5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 74, 2, NULL, 0), (5, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '5', 2, NULL, 0),
(6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 62, 2, NULL, 0), (6, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '6', 2, NULL, 0),
(7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 64, 2, NULL, 0), (7, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '7', 2, NULL, 0),
(8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 75, 1, NULL, 0), (8, 2, 70, 'YEL', 1, 1, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '8', 1, NULL, 0),
(9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, 76, 1, NULL, 0), (9, 2, 70, 'BLU', 1, 2, NULL, 1, 06021010, 0, 2000000000, 0, NULL, 0, '9', 1, NULL, 0),
(10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, 77, 1, NULL, 0), (10, 1, 60, 'YEL', 1, 3, NULL, 1, 05080000, 0, 4751000000, 0, NULL, 0, '10', 1, NULL, 0),
(11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, 78, 2, NULL, 0), (11, 1, 60, 'YEL', 1, 1, NULL, 1, 05080000, 1, 4751000000, 0, NULL, 0, '11', 2, NULL, 0),
(12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, 82, 2, NULL, 0), (12, 3, 30, 'RED', 1, 2, NULL, 2, 06021010, 1, 4751000000, 0, NULL, 0, '12', 2, NULL, 0),
(13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 83, 2, NULL, 0), (13, 5, 30, 'RED', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '13', 2, NULL, 0),
(14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 84, 2, NULL, 0), (14, 5, 90, 'BLU', 1, 2, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0), (15, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, 88, 2, NULL, 0), (16, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 0, 4751000000, 0, NULL, 0, '', 2, NULL, 0),
(71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, 88, 2, NULL, 0); (71, 4, NULL, NULL, NULL, 1, NULL, NULL, 06021010, 1, 4751000000, 0, NULL, 0, '', 2, NULL, 0);
INSERT INTO `vn`.`priceFixed`(`id`, `itemFk`, `rate0`, `rate1`, `rate2`, `rate3`, `started`, `ended`, `bonus`, `warehouseFk`, `created`) INSERT INTO `vn`.`priceFixed`(`id`, `itemFk`, `rate0`, `rate1`, `rate2`, `rate3`, `started`, `ended`, `bonus`, `warehouseFk`, `created`)
VALUES VALUES
@ -2156,7 +2156,7 @@ INSERT INTO `vn`.`campaign`(`code`, `dated`)
INSERT INTO `hedera`.`image`(`collectionFk`, `name`) INSERT INTO `hedera`.`image`(`collectionFk`, `name`)
VALUES VALUES
('user', 'e7723f0b24ff05b32ed09d95196f2f29'); ('user', '4fa3ada0-3ac4-11eb-9ab8-27f6fc3b85fd');
INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`) INSERT INTO `hedera`.`imageCollectionSize`(`id`, `collectionFk`,`width`, `height`)
VALUES VALUES

View File

@ -38,7 +38,7 @@ describe('Travel thermograph path', () => {
it('should select the file to upload', async() => { it('should select the file to upload', async() => {
let currentDir = process.cwd(); let currentDir = process.cwd();
let filePath = `${currentDir}/e2e/dms/ecc/3.jpeg`; let filePath = `${currentDir}/storage/dms/ecc/3.jpeg`;
const [fileChooser] = await Promise.all([ const [fileChooser] = await Promise.all([
page.waitForFileChooser(), page.waitForFileChooser(),

View File

@ -46,7 +46,7 @@ export function directive($timeout) {
$element.on('click', function(event) { $element.on('click', function(event) {
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
let src = $attrs.zoomImage || $attrs.src; let src = $element[0].getAttribute('zoom-image') || $element[0].src;
if (src) if (src)
createContainers(src); createContainers(src);
else else

View File

@ -5,6 +5,24 @@
vn-descriptor-content { vn-descriptor-content {
display: block; display: block;
.photo {
position: relative;
& > img[ng-src] {
min-height: 16em;
display: block;
height: 100%;
width: 100%;
}
vn-float-button {
position: absolute;
margin: 1em;
bottom: 0;
right: 0
}
}
& > vn-spinner { & > vn-spinner {
display: block; display: block;
height: 40px; height: 40px;

View File

@ -13,3 +13,4 @@ import './section';
import './summary'; import './summary';
import './topbar/topbar'; import './topbar/topbar';
import './user-popover'; import './user-popover';
import './upload-photo';

View File

@ -109,14 +109,15 @@ vn-layout {
} }
} }
} }
img { .buttonAccount {
width: 40px;
border-radius: 50%;
}
.buttonAccount {
background: none; background: none;
border: none; border: none;
}
img {
width: 40px;
border-radius: 50%;
}
}
@media screen and (max-width: $mobile-width) { @media screen and (max-width: $mobile-width) {
& > vn-topbar { & > vn-topbar {
& > .start > .logo { & > .start > .logo {

View File

@ -0,0 +1,39 @@
<vn-dialog class="edit"
vn-id="dialog"
on-accept="$ctrl.onUploadAccept()"
message="Upload new photo">
<tpl-body class="upload-photo">
<vn-horizontal ng-show="file.value" class="photo vn-mb-md">
<div><img vn-id="photo" ng-src=""/></div>
</vn-horizontal>
<vn-horizontal>
<vn-textfield
vn-one
label="File name"
ng-model="$ctrl.newPhoto.fileName"
required="true">
</vn-input-file>
</vn-horizontal>
<vn-horizontal>
<vn-input-file vn-id="file"
vn-one
label="File"
ng-model="$ctrl.newPhoto.files"
on-change="$ctrl.updatePhotoPreview(value)"
accept="{{$ctrl.allowedContentTypes}}"
required="true">
<append>
<vn-icon vn-none
color-marginal
title="{{$ctrl.contentTypesInfo}}"
icon="info">
</vn-icon>
</append>
</vn-input-file>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="cancel" translate-attr="{value: 'Cancel'}"/>
<button response="accept" translate>Upload</button>
</tpl-buttons>
</vn-dialog>

View File

@ -0,0 +1,105 @@
import ngModule from '../../module';
import Component from 'core/lib/component';
import './style.scss';
/**
* Small card with basing entity information and actions.
*/
export default class UploadPhoto extends Component {
/**
* Opens the dialog and sets the default data
* @param {*} collection - Collection name
* @param {*} id - Entity id
*/
show(collection, id) {
this.newPhoto = {
id: id,
collection: collection,
fileName: id
};
this.$.dialog.show();
this.getAllowedContentTypes();
}
getAllowedContentTypes() {
this.$http.get('ImageContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes;
});
}
get contentTypesInfo() {
return this.$t('ContentTypesInfo', {
allowedContentTypes: this.allowedContentTypes
});
}
/**
* Updates the image preview
*
* @param {string} value
*/
updatePhotoPreview(value) {
if (value && value[0]) {
const reader = new FileReader();
reader.onload = e => this.$.photo.src = e.target.result;
reader.readAsDataURL(value[0]);
}
}
/**
* Dialog response handler
*
* @return {boolean} Response
*/
onUploadAccept() {
try {
if (!this.newPhoto.files)
throw new Error(`Select an image`);
this.makeRequest();
} catch (e) {
this.vnApp.showError(this.$t(e.message));
return false;
}
return true;
}
/**
* Performs a cancellable request.
*
*/
makeRequest() {
if (this.canceler) this.canceler.resolve();
this.canceler = this.$q.defer();
const options = {
method: 'POST',
url: `Images/upload`,
params: this.newPhoto,
headers: {'Content-Type': undefined},
timeout: this.canceler.promise,
transformRequest: files => {
const formData = new FormData();
for (let i = 0; i < files.length; i++)
formData.append(files[i].name, files[i]);
return formData;
},
data: this.newPhoto.files
};
this.$http(options)
.then(() => this.vnApp.showSuccess(this.$t('Data saved!')))
.then(() => this.emit('response'))
.finally(() => this.canceler = null);
}
}
ngModule.vnComponent('vnUploadPhoto', {
controller: UploadPhoto,
template: require('./index.html'),
bindings: {
data: '<'
}
});

View File

@ -0,0 +1,57 @@
import './index.js';
describe('Salix', () => {
describe('Component vnUploadPhoto', () => {
let controller;
let $scope;
let $httpBackend;
beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, _$httpBackend_) => {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-upload-photo></vn-upload-photo>');
controller = $componentController('vnUploadPhoto', {$element, $scope});
controller.newPhoto = {};
}));
afterEach(() => {
$scope.$destroy();
});
describe('onUploadAccept()', () => {
it('should throw an error message containing "Select an image"', () => {
jest.spyOn(controller.vnApp, 'showError');
controller.onUploadAccept();
expect(controller.vnApp.showError).toHaveBeenCalledWith('Select an image');
});
it('should call to the makeRequest() method', () => {
jest.spyOn(controller, 'makeRequest');
controller.newPhoto.files = [0];
controller.onUploadAccept();
expect(controller.makeRequest).toHaveBeenCalledWith();
});
});
describe('makeRequest()', () => {
it('should make an http query and then emit a response event', () => {
jest.spyOn(controller.vnApp, 'showSuccess');
jest.spyOn(controller, 'emit');
controller.newPhoto.files = [{name: 'hola'}];
$httpBackend.expectRoute('POST', 'Images/upload').respond(200);
controller.makeRequest();
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalledWith('Data saved!');
expect(controller.emit).toHaveBeenCalledWith('response');
});
});
});
});

View File

@ -0,0 +1,3 @@
Upload new photo: Subir una nueva foto
Select an image: Selecciona una imagen
File name: Nombre del fichero

View File

@ -0,0 +1,39 @@
@import "./variables";
.upload-photo {
.photo {
position: relative;
margin: 0 auto;
text-align: center;
& > div {
border: 3px solid $color-primary;
max-width: 256px;
max-height: 256px;
border-radius: 50%;
overflow: hidden
}
& > div > img[ng-src] {
width: 256px;
height: 256px;
display: block;
height: 100%;
width: 100%;
}
}
& > vn-spinner {
display: block;
height: 40px;
padding: $spacing-md;
}
vn-input-file {
max-width: 256px;
div.control {
overflow: hidden
}
}
}

View File

@ -14,7 +14,7 @@
<vn-vertical class="user-popover vn-pa-md"> <vn-vertical class="user-popover vn-pa-md">
<div class="profile-card vn-pb-md"> <div class="profile-card vn-pb-md">
<img <img
ng-src="{{$ctrl.getImageUrl($root.user.id)}}" ng-src="{{::$root.imagePath('user', '160x160', $root.user.id)}}"
on-error-src/> on-error-src/>
<div class="vn-pl-sm"> <div class="vn-pl-sm">
<div> <div>

View File

@ -78,10 +78,6 @@ class Controller {
this.$.companies.refresh(); this.$.companies.refresh();
this.$.popover.show(event.target); this.$.popover.show(event.target);
} }
getImageUrl(userId) {
return '/api/Images/user/160x160/' + userId + '/download?access_token=' + this.vnToken.token;
}
} }
Controller.$inject = ['$scope', '$translate', 'vnConfig', 'vnAuth', 'vnToken']; Controller.$inject = ['$scope', '$translate', 'vnConfig', 'vnAuth', 'vnToken'];

View File

@ -2,14 +2,16 @@ import './index.js';
describe('Salix', () => { describe('Salix', () => {
describe('Component vnUserPopover', () => { describe('Component vnUserPopover', () => {
const userId = 9;
let controller; let controller;
let $scope; let $scope;
let $root;
beforeEach(ngModule('salix')); beforeEach(ngModule('salix'));
beforeEach(inject(($componentController, $rootScope, $httpBackend) => { beforeEach(inject(($componentController, $rootScope, $httpBackend) => {
$httpBackend.expectGET('UserConfigs/getUserConfig'); $httpBackend.expectGET('UserConfigs/getUserConfig');
$root = $rootScope;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
controller = $componentController('vnUserPopover', {$scope}); controller = $componentController('vnUserPopover', {$scope});
})); }));
@ -60,9 +62,10 @@ describe('Salix', () => {
describe('getImageUrl()', () => { describe('getImageUrl()', () => {
it('should return de url image', () => { it('should return de url image', () => {
const url = controller.getImageUrl(); const url = $root.imagePath('user', '160x160', userId);
expect(url).toBeDefined(); expect(url).toBeDefined();
expect(url).toEqual(`/api/Images/user/160x160/${userId}/download?access_token=null`);
}); });
}); });
}); });

View File

@ -7,9 +7,14 @@ export const appName = 'salix';
const ngModule = ng.module('salix', ['vnCore']); const ngModule = ng.module('salix', ['vnCore']);
export default ngModule; export default ngModule;
run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', '$state']; run.$inject = ['$window', '$rootScope', 'vnAuth', 'vnApp', 'vnToken', '$state'];
export function run($window, $rootScope, vnAuth, vnApp, $state) { export function run($window, $rootScope, vnAuth, vnApp, vnToken, $state) {
$rootScope.imagePath = appConfig.imagePath; $rootScope.imagePath = (collection, size, id) => {
if (!collection || !size || !id) return;
const basePath = `/api/Images/${collection}/${size}/${id}`;
return `${basePath}/download?access_token=${vnToken.token}`;
};
$window.validations = {}; $window.validations = {};
vnApp.name = appName; vnApp.name = appName;

View File

@ -0,0 +1,57 @@
const md5 = require('md5');
module.exports = function(Self) {
Self.setup = function() {
Self.super_.setup.call(this);
this.remoteMethod('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
};
/**
* Returns a container instance
* If doesn't exists creates a new one
*
* @param {String} name Container name
* @return {Object} Container instance
*/
Self.container = async function(name) {
const models = Self.app.models;
let container;
try {
container = await models[this.modelName].getContainer(name);
} catch (err) {
if (err.code === 'ENOENT') {
container = await models[this.modelName].createContainer({
name: name
});
} else throw err;
}
return container;
};
Self.getHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
Self.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
Self.allowedContentTypes = async function() {
const connector = this.dataSource.connector;
const allowedContentTypes = connector.allowedContentTypes;
return allowedContentTypes;
};
};

View File

@ -0,0 +1,12 @@
{
"name": "Container",
"base": "VnModel",
"acls": [
{
"property": "status",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
}
]
}

View File

@ -163,5 +163,8 @@
"ASSIGN_ZONE_FIRST": "Asigna una zona primero", "ASSIGN_ZONE_FIRST": "Asigna una zona primero",
"Amount cannot be zero": "El importe no puede ser cero", "Amount cannot be zero": "El importe no puede ser cero",
"Company has to be official": "Empresa inválida", "Company has to be official": "Empresa inválida",
"You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria" "You can not select this payment method without a registered bankery account": "No se puede utilizar este método de pago si no has registrado una cuenta bancaria",
"You can't upload images on the test environment": "No puedes subir imágenes en el entorno de pruebas",
"The selected ticket is not suitable for this route": "El ticket seleccionado no es apto para esta ruta",
"Sorts whole route": "Reordena ruta entera"
} }

View File

@ -1,18 +0,0 @@
const uuid = require('uuid/v1');
const md5 = require('md5');
module.exports = app => {
const storageConnector = app.dataSources.storage.connector;
storageConnector.getFilename = function(file) {
return `${uuid()}.${storageConnector.getFileExtension(file.name)}`;
};
storageConnector.getFileExtension = function(fileName) {
return fileName.split('.').pop().toLowerCase();
};
storageConnector.getPathHash = function(id) {
return md5(id.toString()).substring(0, 3);
};
};

View File

@ -17,11 +17,11 @@
"connectTimeout": 40000, "connectTimeout": 40000,
"acquireTimeout": 20000 "acquireTimeout": 20000
}, },
"storage": { "tempStorage": {
"name": "storage", "name": "tempStorage",
"connector": "loopback-component-storage", "connector": "loopback-component-storage",
"provider": "filesystem", "provider": "filesystem",
"root": "./e2e/dms", "root": "./storage/tmp",
"maxFileSize": "262144000", "maxFileSize": "262144000",
"allowedContentTypes": [ "allowedContentTypes": [
"application/x-7z-compressed", "application/x-7z-compressed",
@ -36,5 +36,37 @@
"image/jpeg", "image/jpeg",
"image/jpg" "image/jpg"
] ]
},
"dmsStorage": {
"name": "dmsStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/dms",
"maxFileSize": "262144000",
"allowedContentTypes": [
"application/x-7z-compressed",
"application/x-zip-compressed",
"application/x-rar-compressed",
"application/octet-stream",
"application/pdf",
"application/zip",
"application/rar",
"multipart/x-zip",
"image/png",
"image/jpeg",
"image/jpg"
]
},
"imageStorage": {
"name": "imageStorage",
"connector": "loopback-component-storage",
"provider": "filesystem",
"root": "./storage/image",
"maxFileSize": "52428800",
"allowedContentTypes": [
"image/png",
"image/jpeg",
"image/jpg"
]
} }
} }

View File

@ -49,5 +49,8 @@
}, },
"Application": { "Application": {
"dataSource": "vn" "dataSource": "vn"
},
"Container": {
"dataSource": "vn"
} }
} }

View File

@ -13,7 +13,7 @@ module.exports = Self => {
}); });
Self.allowedContentTypes = async() => { Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector; const storageConnector = Self.app.dataSources.dmsStorage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes; const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes; const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,4 +1,3 @@
module.exports = Self => { module.exports = Self => {
require('../methods/client-dms/removeFile')(Self); require('../methods/client-dms/removeFile')(Self);
require('../methods/client-dms/allowedContentTypes')(Self);
}; };

View File

@ -26,7 +26,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -62,7 +62,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('clientDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -69,7 +69,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `clientDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -70,8 +70,8 @@
</vn-td> </vn-td>
<vn-td shrink > <vn-td shrink >
<img <img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::buy.image}}" ng-src="{{::$root.imagePath('catalog', '50x50', buy.itemFk)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::buy.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', buy.itemFk)}}"
vn-click-stop vn-click-stop
on-error-src/> on-error-src/>
</vn-td> </vn-td>

View File

@ -33,8 +33,10 @@ module.exports = Self => {
// Exit loop // Exit loop
if (!image) return clearInterval(timer); if (!image) return clearInterval(timer);
const fileName = `${image.itemFk}.png`; const srcFile = image.url.split('/').pop();
const filePath = path.join(tempPath, fileName); const fileName = srcFile.split('.')[0];
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);
const writeStream = fs.createWriteStream(filePath); const writeStream = fs.createWriteStream(filePath);
writeStream.on('open', () => { writeStream.on('open', () => {
@ -57,7 +59,7 @@ module.exports = Self => {
writeStream.on('finish', async function() { writeStream.on('finish', async function() {
try { try {
await models.Image.registerImage('catalog', fileName, filePath); await models.Image.registerImage('catalog', filePath, fileName, image.itemFk);
await image.destroy(); await image.destroy();
} catch (error) { } catch (error) {
await errorHandler(image.itemFk, error, filePath); await errorHandler(image.itemFk, error, filePath);

View File

@ -1,5 +1,5 @@
<vn-portal slot="menu"> <vn-portal slot="menu">
<vn-item-descriptor item="$ctrl.item"></vn-item-descriptor> <vn-item-descriptor item="$ctrl.item" card-reload="$ctrl.reload()"></vn-item-descriptor>
<vn-left-menu source="card"></vn-left-menu> <vn-left-menu source="card"></vn-left-menu>
</vn-portal> </vn-portal>
<ui-view></ui-view> <ui-view></ui-view>

View File

@ -16,19 +16,15 @@
</vn-item> </vn-item>
</slot-menu> </slot-menu>
<slot-before> <slot-before>
<div style="position: relative" text-center> <div class="photo" text-center>
<img <img vn-id="photo"
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}" ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}" zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}"
on-error-src on-error-src/>
/> <vn-float-button ng-click="uploadPhoto.show('catalog', $ctrl.item.id)"
<a href="//verdnatura.es/#!form=admin/items&filter={{$ctrl.item.id}}" target="_blank"> icon="edit"
<vn-float-button vn-visible-by="catalogPhotos">
icon="edit" </vn-float-button>
style="position: absolute; margin: 1em; bottom: 0; right: 0;"
vn-visible-by="marketing, buyer">
</vn-float-button>
</a>
</div> </div>
<vn-horizontal class="item-state"> <vn-horizontal class="item-state">
<vn-one> <vn-one>
@ -102,4 +98,10 @@
</vn-confirm> </vn-confirm>
<vn-worker-descriptor-popover <vn-worker-descriptor-popover
vn-id="workerDescriptor"> vn-id="workerDescriptor">
</vn-worker-descriptor-popover> </vn-worker-descriptor-popover>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -3,6 +3,11 @@ import Descriptor from 'salix/components/descriptor';
import './style.scss'; import './style.scss';
class Controller extends Descriptor { class Controller extends Descriptor {
constructor($element, $, $rootScope) {
super($element, $);
this.$rootScope = $rootScope;
}
get item() { get item() {
return this.entity; return this.entity;
} }
@ -65,13 +70,27 @@ class Controller extends Descriptor {
this.$http.post(`Items/${this.item.id}/clone`) this.$http.post(`Items/${this.item.id}/clone`)
.then(res => this.$state.go('item.card.tags', {id: res.data.id})); .then(res => this.$state.go('item.card.tags', {id: res.data.id}));
} }
onUploadResponse() {
const timestamp = new Date().getTime();
const src = this.$rootScope.imagePath('catalog', '200x200', this.item.id);
const zoomSrc = this.$rootScope.imagePath('catalog', '1600x900', this.item.id);
const newSrc = `${src}&t=${timestamp}`;
const newZoomSrc = `${zoomSrc}&t=${timestamp}`;
this.$.photo.setAttribute('src', newSrc);
this.$.photo.setAttribute('zoom-image', newZoomSrc);
}
} }
Controller.$inject = ['$element', '$scope', '$rootScope'];
ngModule.vnComponent('vnItemDescriptor', { ngModule.vnComponent('vnItemDescriptor', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
item: '<', item: '<',
dated: '<' dated: '<',
cardReload: '&'
} }
}); });

View File

@ -35,8 +35,8 @@
ui-sref="item.card.summary({id: item.id})"> ui-sref="item.card.summary({id: item.id})">
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::item.image}}" ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop vn-click-stop
on-error-src/> on-error-src/>
</vn-td> </vn-td>
@ -44,7 +44,7 @@
<span <span
vn-click-stop="itemDescriptor.show($event, item.id)" vn-click-stop="itemDescriptor.show($event, item.id)"
class="link"> class="link">
{{::item.id | zeroFill:6}} {{::item.id}}
</span> </span>
</vn-td> </vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td> <vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>

View File

@ -11,8 +11,8 @@
<vn-horizontal> <vn-horizontal>
<vn-one> <vn-one>
<img style="width: 100%; display: block;" <img style="width: 100%; display: block;"
ng-src="{{::$root.imagePath}}/catalog/200x200/{{$ctrl.item.image}}" ng-src="{{$root.imagePath('catalog', '200x200', $ctrl.item.id)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{$ctrl.item.image}}" on-error-src/> zoom-image="{{$root.imagePath('catalog', '1600x900', $ctrl.item.id)}}" on-error-src/>
<vn-horizontal class="item-state"> <vn-horizontal class="item-state">
<vn-one> <vn-one>
<p translate>Visible</p> <p translate>Visible</p>

View File

@ -5,11 +5,11 @@
<vn-card> <vn-card>
<div class="image"> <div class="image">
<div ng-if="::item.hex != null" class="item-color-background"> <div ng-if="::item.hex != null" class="item-color-background">
<div class="item-color" style="background-color: #{{::item.hex}}"></div> <div class="item-color" ng-style="{'background-color': '#' + item.hex}"></div>
</div> </div>
<img <img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::item.image}}" ng-src="{{::$root.imagePath('catalog', '200x200', item.id)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::item.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
on-error-src/> on-error-src/>
</div> </div>
<div class="description"> <div class="description">

View File

@ -32,8 +32,8 @@
<vn-tr ng-repeat="row in $ctrl.rows"> <vn-tr ng-repeat="row in $ctrl.rows">
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{::row.item.image}}" ng-src="{{::$root.imagePath('catalog', '50x50', row.item.id)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::row.item.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', row.item.id)}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td number> <vn-td number>

View File

@ -1,5 +1,5 @@
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('guessPriority', { Self.remoteMethodCtx('guessPriority', {
description: 'Changes automatically the priority of the tickets in a route', description: 'Changes automatically the priority of the tickets in a route',
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
@ -19,10 +19,32 @@ module.exports = Self => {
} }
}); });
Self.guessPriority = async id => { Self.guessPriority = async(ctx, id) => {
let query = `CALL vn.routeGuessPriority(?)`; const userId = ctx.req.accessToken.userId;
const $t = ctx.req.__; // $translate
const query = `CALL vn.routeGuessPriority(?)`;
const tx = await Self.beginTransaction({});
let options = [id]; try {
return await Self.rawSql(query, options); let options = {transaction: tx};
const priority = await Self.rawSql(query, [id], options);
let logRecord = {
originFk: id,
userFk: userId,
action: 'update',
changedModel: 'Route',
description: $t('Sorts whole route')
};
await Self.app.models.RouteLog.create(logRecord, options);
await tx.commit();
return priority;
} catch (e) {
await tx.rollback();
throw e;
}
}; };
}; };

View File

@ -1,18 +1,18 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
module.exports = Self => { module.exports = Self => {
Self.remoteMethod('ticketToRoute', { Self.remoteMethod('insertTicket', {
description: 'Check if the ticket can be insert into the route and insert it', description: 'Check if the ticket can be insert into the route and insert it',
accessType: 'READ', accessType: 'READ',
accepts: [{ accepts: [{
arg: 'ticketId', arg: 'routeId',
type: 'number', type: 'number',
required: true, required: true,
description: 'ticketId ', description: 'The route id',
http: {source: 'path'} http: {source: 'path'}
}, },
{ {
arg: 'routeId', arg: 'ticketId',
type: 'number', type: 'number',
required: true required: true
}], }],
@ -21,12 +21,12 @@ module.exports = Self => {
root: true root: true
}, },
http: { http: {
path: `/:ticketId/ticketToRoute`, path: `/:routeId/insertTicket`,
verb: 'PATCH' verb: 'PATCH'
} }
}); });
Self.ticketToRoute = async(ticketId, routeId) => { Self.insertTicket = async(routeId, ticketId) => {
const models = Self.app.models; const models = Self.app.models;
const route = await models.Route.findById(routeId); const route = await models.Route.findById(routeId);
@ -43,9 +43,10 @@ module.exports = Self => {
landed: {between: [minDate, maxDate]}, landed: {between: [minDate, maxDate]},
} }
}); });
if (!ticket) if (!ticket)
throw new UserError('The selected ticket is not suitable for this route'); throw new UserError('The selected ticket is not suitable for this route');
return await ticket.updateAttribute('routeFk', route.id); return ticket.updateAttribute('routeFk', route.id);
}; };
}; };

View File

@ -15,9 +15,15 @@ describe('route guessPriority()', () => {
}); });
it('should call guessPriority() and then check the tickets in the target route now have their priorities defined', async() => { it('should call guessPriority() and then check the tickets in the target route now have their priorities defined', async() => {
const ctx = {
req: {
accessToken: {userId: 9},
__: () => {}
},
};
routeTicketsToRestore = await app.models.Ticket.find({where: {routeFk: targetRouteId}}); routeTicketsToRestore = await app.models.Ticket.find({where: {routeFk: targetRouteId}});
await app.models.Route.guessPriority(targetRouteId); await app.models.Route.guessPriority(ctx, targetRouteId);
let routeTickets = await app.models.Ticket.find({where: {routeFk: targetRouteId}, fields: ['id', 'priority']}); let routeTickets = await app.models.Ticket.find({where: {routeFk: targetRouteId}, fields: ['id', 'priority']});
expect(routeTickets.length).toEqual(2); expect(routeTickets.length).toEqual(2);

View File

@ -1,7 +1,7 @@
const app = require('vn-loopback/server/server'); const app = require('vn-loopback/server/server');
const LoopBackContext = require('loopback-context'); const LoopBackContext = require('loopback-context');
describe('route ticketToRoute()', () => { describe('route insertTicket()', () => {
const deliveryId = 56; const deliveryId = 56;
let originalTicket; let originalTicket;
const routeId = 2; const routeId = 2;
@ -30,7 +30,7 @@ describe('route ticketToRoute()', () => {
originalTicket = await app.models.Ticket.findById(14); originalTicket = await app.models.Ticket.findById(14);
const ticketId = 14; const ticketId = 14;
const result = await app.models.Route.ticketToRoute(ticketId, routeId); const result = await app.models.Route.insertTicket(routeId, ticketId);
expect(result.routeFk).toEqual(2); expect(result.routeFk).toEqual(2);
}); });
@ -40,7 +40,7 @@ describe('route ticketToRoute()', () => {
let error; let error;
try { try {
await app.models.Route.ticketToRoute(ticketId, routeId); await app.models.Route.insertTicket(routeId, ticketId);
} catch (e) { } catch (e) {
error = e.message; error = e.message;
} }

View File

@ -5,7 +5,7 @@ module.exports = Self => {
require('../methods/route/guessPriority')(Self); require('../methods/route/guessPriority')(Self);
require('../methods/route/updateVolume')(Self); require('../methods/route/updateVolume')(Self);
require('../methods/route/getDeliveryPoint')(Self); require('../methods/route/getDeliveryPoint')(Self);
require('../methods/route/ticketToRoute')(Self); require('../methods/route/insertTicket')(Self);
Self.validate('kmStart', validateDistance, { Self.validate('kmStart', validateDistance, {
message: 'Distance must be lesser than 1000' message: 'Distance must be lesser than 1000'

View File

@ -26,7 +26,8 @@
<vn-tbody> <vn-tbody>
<a ng-repeat="route in model.data" <a ng-repeat="route in model.data"
class="clickable vn-tr search-result" class="clickable vn-tr search-result"
ui-sref="route.card.summary({id: {{::route.id}}})"> ui-sref="route.card.summary({id: {{::route.id}}})"
ng-attr-id="{{::route.id}}" vn-droppable="$ctrl.onDrop($event)">
<vn-td shrink> <vn-td shrink>
<vn-check <vn-check
ng-model="route.checked" ng-model="route.checked"

View File

@ -5,6 +5,7 @@ export default class Controller extends Section {
constructor($element, $, vnReport) { constructor($element, $, vnReport) {
super($element, $); super($element, $);
this.vnReport = vnReport; this.vnReport = vnReport;
this.droppableElement = 'a.vn-tr';
} }
preview(route) { preview(route) {
@ -38,6 +39,41 @@ export default class Controller extends Section {
routeId: routesId routeId: routesId
}); });
} }
onDrop($event) {
const target = $event.target;
const droppable = target.closest(this.droppableElement);
const ticketId = $event.dataTransfer.getData('Text');
const routeId = droppable.id;
if (isNaN(ticketId)) {
const regexp = new RegExp(/\/ticket\/([0-9]+)\//i);
const matches = ticketId.match(regexp);
if (matches && matches.length)
this.insert(routeId, matches[1]);
else
this.vnApp.showError(this.$t('Ticket not found'));
}
if (!isNaN(ticketId))
this.insert(routeId, ticketId);
}
insert(routeId, ticketId) {
routeId = parseInt(routeId);
ticketId = parseInt(ticketId);
const query = `Routes/${routeId}/insertTicket`;
return this.$http.patch(query, {ticketId}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh();
}).catch(error => {
if (error.status == 404)
return this.vnApp.showError(this.$t('Ticket not found'));
throw error;
});
}
} }
Controller.$inject = ['$element', '$scope', 'vnReport']; Controller.$inject = ['$element', '$scope', 'vnReport'];

View File

@ -3,10 +3,12 @@ import crudModel from 'core/mocks/crud-model';
describe('Component vnRouteIndex', () => { describe('Component vnRouteIndex', () => {
let controller; let controller;
let $httpBackend;
beforeEach(ngModule('route')); beforeEach(ngModule('route'));
beforeEach(inject($componentController => { beforeEach(inject(($componentController, _$httpBackend_) => {
$httpBackend = _$httpBackend_;
const $element = angular.element('<vn-route-index></vn-route-index>'); const $element = angular.element('<vn-route-index></vn-route-index>');
controller = $componentController('vnRouteIndex', {$element}); controller = $componentController('vnRouteIndex', {$element});
controller.$.model = crudModel; controller.$.model = crudModel;
@ -57,4 +59,83 @@ describe('Component vnRouteIndex', () => {
expect(controller.vnReport.show).toHaveBeenCalledWith('driver-route', expectedParams); expect(controller.vnReport.show).toHaveBeenCalledWith('driver-route', expectedParams);
}); });
}); });
describe('onDrop()', () => {
it('should call the insert method when dragging a ticket number', () => {
jest.spyOn(controller, 'insert');
const routeId = '1';
const expectedTicketId = '16';
const draggedElement = '16';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.insert).toHaveBeenCalledWith(routeId, expectedTicketId);
});
it('should call the insert method when dragging a ticket link', () => {
jest.spyOn(controller, 'insert');
const routeId = '1';
const expectedTicketId = '11';
const draggedElement = 'http://arkamcity.com/#!/ticket/11/summary';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.insert).toHaveBeenCalledWith(routeId, expectedTicketId);
});
it('should throw an error when dragging an invalid ticket link', () => {
jest.spyOn(controller.vnApp, 'showError');
const draggedElement = 'http://arkamcity.com/#!/item/11/summary';
const droppable = document.createElement('a');
droppable.setAttribute('id', 1);
droppable.classList.add('vn-tr');
const $event = {
dataTransfer: {
getData: () => draggedElement
},
target: droppable
};
controller.onDrop($event);
expect(controller.vnApp.showError).toHaveBeenCalledWith('Ticket not found');
});
});
describe('insert()', () => {
it('should make a HTTP patch query and then call both refresh and showSuccess methods', () => {
jest.spyOn(controller.$.model, 'refresh').mockReturnThis();
jest.spyOn(controller.vnApp, 'showSuccess');
const routeId = 1;
const ticketId = 11;
const data = {ticketId};
$httpBackend.expect('PATCH', `Routes/1/insertTicket`, data).respond();
controller.insert(routeId, ticketId);
$httpBackend.flush();
expect(controller.vnApp.showSuccess).toHaveBeenCalled();
expect(controller.$.model.refresh).toHaveBeenCalledWith();
});
});
}); });

View File

@ -31,13 +31,13 @@
model="model"> model="model">
</vn-multi-check> </vn-multi-check>
</vn-th> </vn-th>
<vn-th shrink>Order</vn-th> <vn-th class="order-field">Order</vn-th>
<vn-th number>Ticket</vn-th> <vn-th number>Ticket</vn-th>
<vn-th expand>Client</vn-th> <vn-th expand>Client</vn-th>
<vn-th shrink>Packages</vn-th> <vn-th shrink>Packages</vn-th>
<vn-th number></vn-th> <vn-th shrink></vn-th>
<vn-th>Warehouse</vn-th> <vn-th>Warehouse</vn-th>
<vn-th>Postcode</vn-th> <vn-th translate-attr="{title: 'Postcode'}" shrink>PC</vn-th>
<vn-th expand>Street</vn-th> <vn-th expand>Street</vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
<vn-th shrink></vn-th> <vn-th shrink></vn-th>
@ -50,7 +50,7 @@
ng-model="ticket.checked"> ng-model="ticket.checked">
</vn-check> </vn-check>
</vn-td> </vn-td>
<vn-td shrink> <vn-td class="order-field">
<vn-input-number <vn-input-number
on-change="$ctrl.setPriority(ticket.id, ticket.priority)" on-change="$ctrl.setPriority(ticket.id, ticket.priority)"
ng-model="ticket.priority" ng-model="ticket.priority"
@ -73,9 +73,9 @@
</span> </span>
</vn-td> </vn-td>
<vn-td shrink>{{ticket.packages}}</vn-td> <vn-td shrink>{{ticket.packages}}</vn-td>
<vn-td number>{{::ticket.volume | number:1}}</vn-td> <vn-td shrink>{{::ticket.volume | number:1}}</vn-td>
<vn-td expand>{{ticket.warehouse.name}}</vn-td> <vn-td expand>{{ticket.warehouse.name}}</vn-td>
<vn-td>{{ticket.address.postalCode}}</vn-td> <vn-td shrink>{{ticket.address.postalCode}}</vn-td>
<vn-td expand title="{{ticket.address.street}}">{{ticket.address.street}}</vn-td> <vn-td expand title="{{ticket.address.street}}">{{ticket.address.street}}</vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon <vn-icon

View File

@ -161,11 +161,11 @@ class Controller extends Section {
this.insert(ticketId); this.insert(ticketId);
} }
insert(id) { insert(ticketId) {
const params = {routeId: this.route.id}; ticketId = parseInt(ticketId);
const query = `Routes/${id}/ticketToRoute`;
return this.$http.patch(query, params).then(() => { const query = `Routes/${this.route.id}/insertTicket`;
return this.$http.patch(query, {ticketId}).then(() => {
this.vnApp.showSuccess(this.$t('Data saved!')); this.vnApp.showSuccess(this.$t('Data saved!'));
this.$.model.refresh(); this.$.model.refresh();
this.card.reload(); this.card.reload();

View File

@ -309,8 +309,8 @@ describe('Route', () => {
jest.spyOn(controller.vnApp, 'showSuccess'); jest.spyOn(controller.vnApp, 'showSuccess');
const ticketId = 11; const ticketId = 11;
const data = {routeId: 1}; const data = {ticketId};
$httpBackend.expect('PATCH', `Routes/11/ticketToRoute`, data).respond(); $httpBackend.expect('PATCH', `Routes/1/insertTicket`, data).respond();
controller.insert(ticketId); controller.insert(ticketId);
$httpBackend.flush(); $httpBackend.flush();

View File

@ -7,4 +7,5 @@ Sort routes: Ordenar rutas
Add ticket: Añadir ticket Add ticket: Añadir ticket
Tickets to add: Tickets a añadir Tickets to add: Tickets a añadir
Ticket not found: No se ha encontrado el ticket Ticket not found: No se ha encontrado el ticket
The selected ticket is not suitable for this route: El ticket seleccionado no es apto para esta ruta The selected ticket is not suitable for this route: El ticket seleccionado no es apto para esta ruta
PC: CP

View File

@ -3,4 +3,8 @@
vn-route-tickets form{ vn-route-tickets form{
margin: 0 auto; margin: 0 auto;
max-width: $width-lg; max-width: $width-lg;
.order-field {
max-width: 30px;
}
} }

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,4 +1,3 @@
module.exports = Self => { module.exports = Self => {
require('../methods/ticket-dms/removeFile')(Self); require('../methods/ticket-dms/removeFile')(Self);
require('../methods/ticket-dms/allowedContentTypes')(Self);
}; };

View File

@ -25,7 +25,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -67,7 +67,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -16,7 +16,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('ticketDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -69,7 +69,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `ticketDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -72,7 +72,7 @@ Notes: Notas
Volume: Volumen Volume: Volumen
Expedition: Expedición Expedition: Expedición
New state: Nuevo estado New state: Nuevo estado
Packages: Embalajes Packages: Bultos
Tracking: Estados Tracking: Estados
Sale checked: Control clientes Sale checked: Control clientes
Components: Componentes Components: Componentes

View File

@ -19,8 +19,8 @@
<vn-card> <vn-card>
<div class="image"> <div class="image">
<img <img
ng-src="{{::$root.imagePath}}/catalog/200x200/{{::sale.item.image}}" ng-src="{{::$root.imagePath('catalog', '200x200', sale.itemFk)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{::sale.item.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/> on-error-src/>
</div> </div>
<div class="description"> <div class="description">

View File

@ -93,8 +93,8 @@
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$root.imagePath}}/catalog/50x50/{{sale.image}}" ng-src="{{::$root.imagePath('catalog', '50x50', sale.itemFk)}}"
zoom-image="{{::$root.imagePath}}/catalog/1600x900/{{sale.image}}" zoom-image="{{::$root.imagePath('catalog', '1600x900', sale.itemFk)}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td number> <vn-td number>

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,5 +1,4 @@
module.exports = Self => { module.exports = Self => {
require('../methods/travel-thermograph/allowedContentTypes')(Self);
require('../methods/travel-thermograph/getThermographTemperatures')(Self); require('../methods/travel-thermograph/getThermographTemperatures')(Self);
}; };

View File

@ -21,7 +21,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });
@ -46,7 +46,7 @@ class Controller extends Section {
warehouseId: warehouseId, warehouseId: warehouseId,
companyId: companyId, companyId: companyId,
dmsTypeId: dmsTypeId, dmsTypeId: dmsTypeId,
description: this.$t('FileDescription', { description: this.$t('TravelFileDescription', {
travelId: this.travel.id travelId: this.travel.id
}).toUpperCase() }).toUpperCase()
}; };

View File

@ -53,7 +53,7 @@ describe('Ticket', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['application/pdf', 'image/png', 'image/jpg']; const expectedResponse = ['application/pdf', 'image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('TravelThermographs/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -79,7 +79,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `TravelThermographs/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -8,7 +8,7 @@ Upload file: Subir fichero
Edit file: Editar fichero Edit file: Editar fichero
Upload: Subir Upload: Subir
File: Fichero File: Fichero
FileDescription: Travel id {{travelId}} TravelFileDescription: Travel id {{travelId}}
ContentTypesInfo: 'Tipos de archivo permitidos: {{allowedContentTypes}}' ContentTypesInfo: 'Tipos de archivo permitidos: {{allowedContentTypes}}'
Are you sure you want to continue?: ¿Seguro que quieres continuar? Are you sure you want to continue?: ¿Seguro que quieres continuar?
Add thermograph: Añadir termógrafo Add thermograph: Añadir termógrafo

View File

@ -146,7 +146,7 @@ module.exports = Self => {
}); });
if (currentContract) { if (currentContract) {
const maxHolidays = currentContract.holidays().days; const maxHolidays = currentContract.holidays() && currentContract.holidays().days;
calendar.totalHolidays = maxHolidays; calendar.totalHolidays = maxHolidays;
workedDays -= entitlementRate; workedDays -= entitlementRate;

View File

@ -1,23 +0,0 @@
module.exports = Self => {
Self.remoteMethodCtx('allowedContentTypes', {
description: 'Returns a list of allowed contentTypes',
accessType: 'READ',
returns: {
type: ['Object'],
root: true
},
http: {
path: `/allowedContentTypes`,
verb: 'GET'
}
});
Self.allowedContentTypes = async() => {
const storageConnector = Self.app.dataSources.storage.connector;
const allowedContentTypes = storageConnector.allowedContentTypes;
const modelAllowedContentTypes = Self.definition.settings.allowedContentTypes;
return modelAllowedContentTypes || allowedContentTypes;
};
};

View File

@ -1,7 +1,6 @@
module.exports = Self => { module.exports = Self => {
require('../methods/worker-dms/downloadFile')(Self); require('../methods/worker-dms/downloadFile')(Self);
require('../methods/worker-dms/removeFile')(Self); require('../methods/worker-dms/removeFile')(Self);
require('../methods/worker-dms/allowedContentTypes')(Self);
require('../methods/worker-dms/filter')(Self); require('../methods/worker-dms/filter')(Self);
Self.isMine = async function(ctx, dmsId) { Self.isMine = async function(ctx, dmsId) {

View File

@ -26,7 +26,7 @@
<h6 translate>Holidays</h6> <h6 translate>Holidays</h6>
<div> <div>
{{'Used' | translate}} {{$ctrl.calendar.holidaysEnjoyed}} {{'Used' | translate}} {{$ctrl.calendar.holidaysEnjoyed}}
{{'of' | translate}} {{$ctrl.calendar.totalHolidays}} {{'days' | translate}} {{'of' | translate}} {{$ctrl.calendar.totalHolidays || 0}} {{'days' | translate}}
</div> </div>
</div> </div>
<div class="vn-pt-md"> <div class="vn-pt-md">

View File

@ -1,6 +1,18 @@
<vn-descriptor-content <vn-descriptor-content
module="worker" module="worker"
description="$ctrl.worker.firstName +' '+ $ctrl.worker.lastName"> description="$ctrl.worker.firstName +' '+ $ctrl.worker.lastName">
<slot-before>
<div class="photo" text-center>
<img vn-id="photo"
ng-src="{{$root.imagePath('user', '520x520', $ctrl.worker.id)}}"
zoom-image="{{$root.imagePath('user', '1600x1600', $ctrl.worker.id)}}"
on-error-src/>
<vn-float-button ng-click="uploadPhoto.show('user', $ctrl.worker.id)"
icon="edit"
vn-visible-by="userPhotos">
</vn-float-button>
</div>
</slot-before>
<slot-body> <slot-body>
<div class="attributes"> <div class="attributes">
<vn-label-value <vn-label-value
@ -42,4 +54,10 @@
<div ng-transclude="btnThree"></div> <div ng-transclude="btnThree"></div>
</div> </div>
</slot-body> </slot-body>
</vn-descriptor-content> </vn-descriptor-content>
<!-- Upload photo dialog -->
<vn-upload-photo
vn-id="uploadPhoto"
on-response="$ctrl.onUploadResponse()">
</vn-upload-photo>

View File

@ -2,6 +2,11 @@ import ngModule from '../module';
import Descriptor from 'salix/components/descriptor'; import Descriptor from 'salix/components/descriptor';
class Controller extends Descriptor { class Controller extends Descriptor {
constructor($element, $, $rootScope) {
super($element, $);
this.$rootScope = $rootScope;
}
get worker() { get worker() {
return this.entity; return this.entity;
} }
@ -48,8 +53,21 @@ class Controller extends Descriptor {
return this.getData(`Workers/${this.id}`, {filter}) return this.getData(`Workers/${this.id}`, {filter})
.then(res => this.entity = res.data); .then(res => this.entity = res.data);
} }
onUploadResponse() {
const timestamp = new Date().getTime();
const src = this.$rootScope.imagePath('user', '520x520', this.worker.id);
const zoomSrc = this.$rootScope.imagePath('user', '1600x900', this.worker.id);
const newSrc = `${src}&t=${timestamp}`;
const newZoomSrc = `${zoomSrc}&t=${timestamp}`;
this.$.photo.setAttribute('src', newSrc);
this.$.photo.setAttribute('zoom-image', newZoomSrc);
}
} }
Controller.$inject = ['$element', '$scope', '$rootScope'];
ngModule.vnComponent('vnWorkerDescriptor', { ngModule.vnComponent('vnWorkerDescriptor', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller, controller: Controller,

View File

@ -26,7 +26,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('workerDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -64,7 +64,7 @@ describe('Client', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `workerDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -17,7 +17,7 @@ class Controller extends Section {
} }
getAllowedContentTypes() { getAllowedContentTypes() {
this.$http.get('WorkerDms/allowedContentTypes').then(res => { this.$http.get('DmsContainers/allowedContentTypes').then(res => {
const contentTypes = res.data.join(', '); const contentTypes = res.data.join(', ');
this.allowedContentTypes = contentTypes; this.allowedContentTypes = contentTypes;
}); });

View File

@ -70,7 +70,7 @@ describe('Worker', () => {
describe('getAllowedContentTypes()', () => { describe('getAllowedContentTypes()', () => {
it('should make an HTTP GET request to get the allowed content types', () => { it('should make an HTTP GET request to get the allowed content types', () => {
const expectedResponse = ['image/png', 'image/jpg']; const expectedResponse = ['image/png', 'image/jpg'];
$httpBackend.expect('GET', `WorkerDms/allowedContentTypes`).respond(expectedResponse); $httpBackend.expect('GET', `DmsContainers/allowedContentTypes`).respond(expectedResponse);
controller.getAllowedContentTypes(); controller.getAllowedContentTypes();
$httpBackend.flush(); $httpBackend.flush();

View File

@ -60,7 +60,7 @@
</vn-icon-button> </vn-icon-button>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<vn-icon-button ui-sref="worker.card.edit({dmsId: {{::document.dmsFk}}})" <vn-icon-button ui-sref="worker.card.dms.edit({dmsId: {{::document.dmsFk}}})"
icon="edit" icon="edit"
title="{{'Edit file' | translate}}"> title="{{'Edit file' | translate}}">
</vn-icon-button> </vn-icon-button>

View File

@ -43,6 +43,6 @@
"pool": true "pool": true
}, },
"storage": { "storage": {
"root": "./e2e/dms" "root": "./storage/dms"
} }
} }

Some files were not shown because too many files have changed in this diff Show More