modal ready

parent af33675d
......@@ -10,3 +10,5 @@ Para actualizar instalaciones antiguas ejecutar en vagrant/roles/database/files
mysql -u root -p pictodb < upgrade.sql
Relanzar trigger-enrolments-integrity-constraints
En sails/src lanzar `npm install` para instalar nuevos paquetes
......@@ -344,14 +344,12 @@ module.exports = {
* ]
*/
supervisors: function (req, res) {
if (!req.params.id_stu) {
return res.json(500, {
error: 'No student defined'
});
}
if (!req.params.id_stu)
return res.badRequest('No student defined');
Student.validSupervisors(req.params.id_stu, req.token.id, function (err, sups) {
if (err) throw err;
return res.json(sups);
if (err) return res.serverError(err);
return res.ok(sups);
});
},
......
/**
* TPVController
*
* @description :: Server-side controller for preparing TPV interactions
* @help :: See http://links.sailsjs.org/docs/controllers
*/
//const nacl_util = require('tweetnacl');
const nacl_util = require('tweetnacl-util');
const sha256 = require("fast-sha256");
module.exports = {
/**
* Return form data to client to initiate TPV transaction
*/
init: function(req, res) {
var params = req.allParams();
if (!params.id_stu || !params.id_sup || !params.type)
return res.badRequest();
var amount = sails.config.pictogram.tpv.prices.oneYearEuro;
if (params.type == 'forever')
amount = sails.config.pictogram.tpv.prices.foreverEuro;
var tpvdata =
{
Ds_Merchant_PayMethods: 'O', // Payment managed by RedSys
Ds_Merchant_MerchantCode: sails.config.pictogram.tpv.merchantCode, // Number of commerce (Yotta)
Ds_Merchant_Terminal: '001', // Terminal number
Ds_Merchant_Currency: '000 ("978")', // Terminal currency
Ds_Merchant_TransactionType: , // Type of the transaction
Ds_Merchant_Amount: amount, // Amount
Ds_Merchant_MerchantUrl: sails.getBaseUrl() + "/tpv/notify",
Ds_Merchant_UrlOk: sails.getBaseUrl() + '/app/#/student/' + params.id_stu + "/setup/renewed/1" // Returning URL (success)
Ds_Merchant_UrlKo: sails.getBaseUrl() + '/app/#/student/' + params.id_stu + "/setup/renewed/0" // Returning URL (error)
};
console.log(JSON.stringify(tpvdata));
// Parameters in Base64
var merchantParameters = nacl_util.encodeBase64(JSON.stringify(tpvdata));
// HMAC 256 signature
var signature = nacl_util.encodeBase64(sha256.hmac(sails.config.pictogram.tpv.key, merchantParameters));
res.ok({
merchantParameters: merchantParameters,
signatureVersion: "HMAC_SHA256_V1",
signature: signature
});
},
/**
* Notification of transaction from RedSys
*/
notify: function(req, res) {
var params = req.allParams();
var received_signature = params.Ds_Signature;
var calculated_signature = nacl_util.encodeBase64(sha256.hmac(sails.config.pictogram.tpv.key, params.Ds_MerchantParameters));
if (received_signature == calculated_signature) {
console.log("OK");
// TBD: CREAR NUEVA LICENCIA
}
else
console.log("ERROR");
console.log(JSON.stringify(params));
}
};
......@@ -314,7 +314,7 @@ module.exports = {
throw new Error("No supervisors");
// return supervisors and related supervisors to the office
return [stuSups, SupOff.find({office: id_sup}).populate('supervisor')]
return ([stuSups, SupOff.find({office: id_sup}).populate('supervisor')]);
})
.spread((stuSups, supOffs) => {
......@@ -323,10 +323,11 @@ module.exports = {
// student is valid
var supIdx = stuSups.findIndex(x => x.supervisor && x.supervisor.id == id_sup);
if (supIdx >= 0)
return callback(null, [stuSups[supIdx].supervisor]);
return ([stuSups[supIdx].supervisor]); // break then chain
throw new Error("No supervisors related");
}
// else (continue then chain)
// filter null entries and map them to the supervisor object
var ss = _.compact(_.compact(stuSups).map(x => x.supervisor)); // supervisors linked to student
......@@ -335,16 +336,16 @@ module.exports = {
// filter from the second list those found in the first list
var sups = so.filter(a => ss.findIndex(b => b.id == a.id) >= 0);
return [sups, Supervisor.find(id_sup)];
})
.spread((sups, me) => {
// The requester is an office, so it has to be appended to the list of valid supervisors
sups.push(me[0]);
return callback(null, sups);
return $.when([sups, Supervisor.find(id_sup)])
.spread((sups, me) => {
// The requester is an office, so it has to be appended to the list of valid supervisors
sups.push(me[0]);
return sups;
})
.catch((err) => {throw err});
})
.catch((err) => {
return callback(err, []);
});
.then((sups) => {return callback(null, sups)})
.catch((err) => {return callback(err, [])});
},
......
......@@ -30,9 +30,7 @@
"ngMask": "angular-mask#~3.1.1",
"angular-recaptcha": "^4.0.1",
"ui-bootstrap": "~2.5.0",
"ngInfiniteScroll": "^1.3.4",
"fast-sha256": "^1.0.0",
"tweetnacl-util": "^0.15.0"
"ngInfiniteScroll": "^1.3.4"
},
"resolutions": {
"angular": ">=1 <1.3.0",
......
......@@ -55,6 +55,7 @@
"beep": "Beep",
"birthdate": "Birthdate",
"black_and_white": "B&W",
"buy": "Buy",
"cancel": "Cancel",
"cant_delete_active_scene": "Active scene could not be deleted, deactive it first",
"cannot_delete_method": "Method could not be deleted, maybe due to existing recorded sessions.",
......@@ -214,6 +215,8 @@
"license_missing_trial": "Trial license expired. Buy an official one to keep using this account (go to Settings).",
"license_number": "License number",
"license_pro": "Pictogram PRO license",
"license_forever": "Pictogram PRO license (forever)",
"license_oneyear": "Pictogram 12-months license",
"license_warning": "Only available for professional licenses (Pictogram Pro).",
"licenses_created": "Licenses created",
"licenses_left": "{{number}} licenses left",
......@@ -266,7 +269,7 @@
"no_office": "No office",
"no_subscribed": "No connection to student account",
"no_students": "You are not linked to any student",
"no_students_desc": "Click on 'Add student' to link to existing student accounts or to create new ones",
"no_students_desc": "Click on 'Add student' to link to existing student accounts or to create new ones",
"no_students_for_user": "You are not associated to any students. Please ask your office to link your account to a Pictogram student.",
"no_space_in_category": "No space left in category",
"no_supervisors": "No supervisors linked. ",
......@@ -455,6 +458,9 @@
"tpl_date_frame": "de {{ begin | date:'dd-MM-yyyy' }} a {{ end | date:'dd-MM-yyyy' }}",
"tpl_day": "{{ day | date:'yyyy-MM-dd' }}",
"tpl_hours_frame": "from {{ begin | date:'HH:mm:ss' }} to {{ end | date:'HH:mm:ss' }}",
"tpv_init_error": "Unable to initiate payment process",
"tpv_success": "The new license is now active",
"tpv_title": "Select license type",
"trial_license": "Trial license",
"tries": "Tries",
"tries_done": "Tries done",
......
......@@ -55,6 +55,7 @@
"beep": "Pitido",
"birthdate": "Fecha de nacimiento",
"black_and_white": "ByN",
"buy": "Comprar",
"cancel": "Cancelar",
"cant_delete_active_scene": "No se puede eliminar la escena activa, desactívela antes.",
"cannot_delete_method": "No se pudo eliminar el método, tal vez porque existen sesiones asociadas.",
......@@ -212,6 +213,8 @@
"license_invalid": "Licencia inválida",
"license_number": "Número de licencia",
"license_pro": "Licencia Pictogram PRO",
"license_forever": "Licencia Pictogram PRO (ilimitada)",
"license_oneyear": "Licencia Pictogram de 12 meses",
"license_warning": "Sólo disponible para licencias profesionales (Pictogram Pro).",
"license_missing_official": "La licencia PRO expiró. El acceso a las funcionalidades terapéuticas queda restringido, aunque puede seguir gestionando pictogramas y dispositivos.",
"license_missing_trial": "La licencia de prueba expiró. Adquiera una licencia oficial para mantener esta cuenta (puede hacerlo en Configuración).",
......@@ -453,6 +456,9 @@
"tpl_date_frame": "de {{ begin | date:'dd-MM-yyyy' }} a {{ end | date:'dd-MM-yyyy' }}",
"tpl_day": "{{ day | date:'dd-MM-yyyy' }}",
"tpl_hours_frame": "de {{ begin | date:'HH:mm:ss' }} a {{ end | date:'HH:mm:ss' }}",
"tpv_init_error": "No se pudo iniciar el proceso de compra",
"tpv_success": "La nueva licencia ha sido activada",
"tpv_title": "Seleccione el tipo de licencia",
"trial_license": "Licencia de prueba",
"tries": "Ensayos",
"tries_done": "Ensayos realizados",
......
......@@ -28,9 +28,6 @@ dashboardApp.constant('CONSTANTS', {
appName: 'Pictogram Dashboard',
appVersion: 0.1,
password_minlength: 8,
tpvKey: 'sq7HjrUOBfKmC576ILgskD5srU870gJ7',
tpvPriceOneYearEuros: 70,
tpvPriceForeverEuros: 120,
});
......
......@@ -231,42 +231,35 @@ dashboardControllers.controller('StudentCollectionsCtrl', function StudentCollec
//If scene is viewingScene, load activeScene
$scope.delete_scene = function (scene) {
$translate('confirmation').then(t => {
if ($window.confirm(t)) {
if(!scene.active){ // only delete if scene not active
$http.delete(config.backend + '/scene/' + scene.id + '/stu/' + $scope.studentData.id)
.success(function () {
//Socket notify
io.socket.post('/scene', {
action: 'delete',
scene: {id:scene.id}
}, function () {});
//Reload active scene
if(scene == $scope.viewingScene){
$scope.showActiveScene();
}
if (!$window.confirm($translate.instant('confirmation')))
return;
//Reload scenes list
$scope.loadScenesList();
if(!scene.active){ // only delete if scene not active
$http.delete(config.backend + '/scene/' + scene.id + '/stu/' + $scope.studentData.id)
.success(function () {
$translate('scene_deleted').then(function (translation) {
ngToast.success({ content: translation });
});
//Socket notify
io.socket.post('/scene', {
action: 'delete',
scene: {id:scene.id}
}, function () {});
}).error(function () {
$translate('scene_already_deleted').then(function (translation) {
ngToast.warning({ content: translation });
});
});
}else{ // if scene active, show warning
$translate('cant_delete_active_scene').then(function (translation) {
ngToast.warning({ content: translation });
});
//Reload active scene
if(scene == $scope.viewingScene){
$scope.showActiveScene();
}
}
});
//Reload scenes list
$scope.loadScenesList();
ngToast.success($translate.instant('scene_deleted'));
}).error(function () {
ngToast.warning($translate.instant('scene_already_deleted'));
});
} else { // if scene active, show warning
ngToast.warning($translate.instant('cant_delete_active_scene'));
}
};
// Update student scene
......@@ -288,10 +281,7 @@ dashboardControllers.controller('StudentCollectionsCtrl', function StudentCollec
scene: data
}, function () {});
$translate('scene_updated').then(function (translation) {
ngToast.success({ content: translation });
});
ngToast.success($translate.instant('scene_updated'));
$scope.loadScenesList();
}).error(function () {});
......@@ -301,21 +291,18 @@ dashboardControllers.controller('StudentCollectionsCtrl', function StudentCollec
// Activate student scene
$scope.activate_scene = function (scene) {
$http.put(config.backend + '/stu/' + $scope.studentData.id + '/activeScene/' + scene.id, {id_scene:scene.id})
.success(function (stu) {
io.socket.post('/stu/config', {
action: 'update',
attributes: stu
}, function () {});
$http.put(config.backend + '/stu/' + $scope.studentData.id + '/activeScene/' + scene.id, {id_scene:scene.id})
.success(function (stu) {
$translate('scene_updated').then(function (translation) {
ngToast.success({ content: translation });
});
io.socket.post('/stu/config', {
action: 'update',
attributes: stu
}, function () {});
$scope.loadScenesList();
ngToast.success($translate.instant('scene_updated'));
$scope.loadScenesList();
}).error(function () {});
}).error(function () {});
};
// Duplicate viewing scene
......@@ -328,40 +315,35 @@ dashboardControllers.controller('StudentCollectionsCtrl', function StudentCollec
action: 'add',
data: newScene
}, function () {});
ngToast.success($translate.instant('scene_duplicated'));
$scope.loadScenesList();
$translate('scene_duplicated').then(function (translation) {
ngToast.success({ content: translation });
});
}).error(function () {});
};
$scope.deleteFreePicto = function (studentPicto) {
$translate('confirmation').then(t => {
if ($window.confirm(t)) {
$http.delete(config.backend + '/stu/' + $scope.studentData.id + '/picto/' + studentPicto.id)
.success(function () {
$scope.freeCategoryPictos
[studentPicto.attributes.free_category_coord_x]
[studentPicto.attributes.free_category_coord_y] = $scope.emptyStudentPicto;
if (!$window.confirm($translate.instant('confirmation')))
return;
io.socket.post('/stu/vocabulary', {
action: 'delete',
attributes: {
id_stu: $scope.studentData.id,
id_scene: $scope.viewingScene.id,
stu_picto: studentPicto
}
}, function () {});
$http.delete(config.backend + '/stu/' + $scope.studentData.id + '/picto/' + studentPicto.id)
.success(function () {
$scope.freeCategoryPictos
[studentPicto.attributes.free_category_coord_x]
[studentPicto.attributes.free_category_coord_y] = $scope.emptyStudentPicto;
$translate('picto_removed').then(function (translation) {
ngToast.success({ content: translation });
});
io.socket.post('/stu/vocabulary', {
action: 'delete',
attributes: {
id_stu: $scope.studentData.id,
id_scene: $scope.viewingScene.id,
stu_picto: studentPicto
}
}, function () {});
}).error(function () {});
}
});
ngToast.success($translate.instant('picto_removed'));
}).error(function () {});
};
// View student picto
......
......@@ -6,6 +6,7 @@
//-----------------------
dashboardControllers.controller('StudentSetupCtrl', function StudentSetupCtrl(
$scope,
$rootScope,
$http,
config,
$stateParams,
......@@ -16,9 +17,16 @@ dashboardControllers.controller('StudentSetupCtrl', function StudentSetupCtrl(
$translate,
lodash,
ngToast) {
// For tab navigation (here too, if the user refresh the page...)
$scope.nav.tab = 'setup';
// If we get with this parameter, we're back from a TPV transaction!
if ($stateParams.renewed && $stateParams.renewed == 1)
ngToast.success($translate.instant('tpv_success'));
if ($stateParams.renewed && $stateParams.renewed == 0)
ngToast.success($translate.instant('tpv_error'));
//
// Student setup
//
......@@ -259,58 +267,21 @@ dashboardControllers.controller('StudentSetupCtrl', function StudentSetupCtrl(
// ---------------------------------------------------------------------------
// TPV modal window
//
var modalInstance = $modal.open({
animation: true,
templateUrl: 'modules/student/views/tpvmodal.html',
controller: 'TPVModalCtrl',
size: 'lg',
resolve: {
student: function () {
return $scope.studentData;
},
supervisor: function () {
return $rootScope.user;
}
}
});
// Returned data from the modal window
modalInstance.result.then(function (pictoId) {
if(!pictoId)
return;
// Send the picto to the server
$http.post(config.backend + '/stu/' + $scope.studentData.id + '/picto/' + pictoId, {
attributes: {
id_cat: ($scope.showFreeCategory || mainCatGrid) ? null : $scope.getCategoryId($scope.selectedCategory),
coord_x: $scope.showFreeCategory ? null : col,
coord_y: $scope.showFreeCategory ? null : row,
status: 'enabled',
free_category_coord_x: $scope.showFreeCategory ? col : null,
free_category_coord_y: $scope.showFreeCategory ? row : null
},
id_scene: $scope.viewingScene.id
})
.success(function (studentPicto) {
placePicto(studentPicto);
io.socket.post('/stu/vocabulary', {
action: 'add',
attributes: {
id_stu: $scope.studentData.id,
id_scene: $scope.viewingScene.id,
stu_picto: studentPicto
$scope.openBuyModal = function() {
var modalInstance = $modal.open({
animation: true,
templateUrl: 'modules/student/views/tpvmodal.html',
controller: 'TPVModalCtrl',
size: 'lg',
resolve: {
student: function () {
return $scope.studentData;
},
supervisor: function () {
return $rootScope.user;
}
}, function () {});
})
.error(function (err) {
if (err.code && err.code == 1) // codes are in sails/config/pictogram.js
ngToast.danger({ content: $translate.instant('error_duplicated_picto') });
else
ngToast.danger({ content: $translate.instant('error_adding_picto') });
}
});
});
}
});
......@@ -3,44 +3,40 @@
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
dashboardControllers.controller('TPVModalCtrl', function ($scope, $modalInstance, $http, CONSTANTS, config, supervisor, student) {
$scope.foreverPrice = CONSTANTS.tpvForeverPriceEuros;
$scope.oneYearPrice = CONSTANTS.tpvOneYearPriceEuros;
var tpvdata =
{
Ds_Merchant_PayMethods: 'O', // Payment managed by RedSys
Ds_Merchant_MerchantCode: '152038485', // Number of commerce (Yotta)
Ds_Merchant_Terminal: '001', // Terminal number
Ds_Merchant_Currency: '000 ("978")', // Terminal currency
Ds_Merchant_Amount: CONSTANTS.tpvAnualPrice, // Amount
Ds_Merchant_MerchantUrl: config.backend + "/stu/buypro",
Ds_Merchant_UrlOk: config.backend + '/app/#/student' + student.id + "/setup", // Returning URL
};
// HMAC SHA256 encryption in Base64
var merchantParameters = nacl.util.encodeBase64(sha256.hmac(CONSTANTS.tpvKey, JSON.stringify(tpvdata)));
// Save expression
$scope.sendToRedSys = function () {
dashboardControllers.controller('TPVModalCtrl', function (
$scope,
$modalInstance,
$http,
ngToast,
$translate,
config,
supervisor,
student) {
$scope.oneYearPrice = 70;
$scope.foreverPrice = 175;
$scope.buyLicense = function(type) {
// Get encrypted data from our server
$http
.post('https://sis-t.redsys.es:25443/sis/realizarPago',
{
'Ds_SignatureVersion': "HMAC_SHA256_V1",
'Ds_MerchantParameters': merchantParameters,
'Ds_Signature': signature,
})
.post(config.backend + '/tpv/init', {
id_stu: student.id,
id_sup: supervisor.id,
type: type
})
.success(function(data, status, headers, config) {
console.log("Expression changed: " + JSON.stringify(data));
// Close the modal instance
$modalInstance.close(data);
// Non-AJAX post to RedSys
var form = $('<form id="redsysform" action="https://sis-t.redsys.es:25443/sis/realizarPago" method="POST">' +
'<input type="hidden" name="Ds_SignatureVersion" value="' + data.signatureVersion + '">' +
'<input type="hidden" name="Ds_MerchantParameters" value="' + data.merchantParameters + '">' +
'<input type="hidden" name="Ds_Signature" value="' + data.signature + '">' +
'</form>');
$(document.body).append(form);
$("#redsysform").submit();
})
.error(function(data, status, headers, config) {
console.log("Error from API: " + data.error);
ngToast.danger($translate.instant('tpv_init_error'));
});
};
$scope.close = function () {
......
......@@ -107,6 +107,8 @@
</div>
</div>
<button type="submit" class="btn btn-primary" ng-click="openBuyModal()" translate>renew</button>
</div>
</fieldset>
......
......@@ -10,25 +10,25 @@
<form name="tpvForm">
<div class="row">
<div class="md-col-6">
<h4 translate>one_year_license</h4>
<h1 class="tpv-price">{{ oneYearPrice }} €</h1>
<div class="col-md-6">
<h4 translate class="text-center">license_oneyear</h4>
<h1 class="tpv-price text-info text-center">{{ oneYearPrice }} €</h1>
</div>
<div class="md-col-6">
<h4 translate>forever_license</h4>
<h1 class="tpv-price">{{ foreverPrice }} €</h1>
<div class="col-md-6">
<h4 translate class="text-center">license_forever</h4>
<h1 class="tpv-price text-info text-center">{{ foreverPrice }} €</h1>
</div>
</div>
<div class="row">
<div class="md-col-6">
<span class="input-group-btn">
<button class="btn btn-success" ng-click="sendToRedSys('oneYear')" translate><span class="glyphicon glyphicon-shopping-cart" aria-hidden="true"></span> buy</button>
<div class="col-md-6">
<span class="input-group-btn text-center">
<button class="btn btn-success" ng-click="buyLicense('oneYear')"><span class="glyphicon glyphicon-shopping-cart" aria-hidden="true"></span> {{ 'buy' | translate }} </button>
</span>
</div>
<div class="md-col-6">
<span class="input-group-btn">
<button class="btn btn-success" ng-click="sendToRedSys('forever')" translate><span class="glyphicon glyphicon-shopping-cart" aria-hidden="true"></span> buy</button>
<div class="col-md-6">
<span class="input-group-btn text-center">
<button class="btn btn-success" ng-click="buyLicense('forever')"><span class="glyphicon glyphicon-shopping-cart" aria-hidden="true"></span> {{ 'buy' | translate}}</button>
</span>
</div>
</div>
......
......@@ -24,6 +24,23 @@ module.exports.pictogram = {
trial_license_duration: 3, // number of moths the trial license is valid
password_minlength: 8, // minimal size for the password string
//
// TPV parameters
//
tpv: {
prices: {
foreverEuro: 175, // non limited license in euros
oneYearEuro: 70, // one year license in euros
},
merchantCode: '152038485', // Code for Yotta
merchantKey: 'qwertyasdf0123456789',
key: 'sq7HjrUOBfKmC576ILgskD5srU870gJ7' // key for signing
},
//
// URLs
//
urls: {
/**
* Gets the public url of a supervisor avatar
......
......@@ -168,5 +168,10 @@ module.exports.policies = {
create: ['tokenAuth'],
update: ['tokenAuth'],
close: ['tokenAuth']
},
TPVController: {
init: ['tokenAuth'],
notify: true,
}
};
......@@ -146,5 +146,8 @@ module.exports.routes = {
'GET /ws/:id_ws/tries': 'WorkingSessionController.tries',
'PUT /ws/:id': 'WorkingSessionController.update',
'POST /ws': 'WorkingSessionController.create',
'POST /ws/:id_ws/close': 'WorkingSessionController.close'
'POST /ws/:id_ws/close': 'WorkingSessionController.close',
'POST /tpv/init': 'TPVController.init',
'POST /tpv/notify': 'TPVController.notify'
};
......@@ -11,6 +11,7 @@
"connect-redis": "3.0.2",
"connect-timeout": "^1.7.0",
"ejs": "^0.8.8",
"fast-sha256": "^1.0.0",
"forever": "^0.14.1",
"grunt": "^1.0.1",
"grunt-contrib-clean": "^1.0.0",
......@@ -22,6 +23,7 @@
"include-all": "~0.1.3",
"jsonwebtoken": "~0.4.0",
"lodash": "^3.10.1",
"moment": "^2.18.1",
"rc": "~0.5.0",
"sails": "^0.12.3",
"sails-disk": "~0.10.0",
......@@ -31,6 +33,7 @@
"sails-test-helper": "^0.3.5",
"socket.io": "~1.3.2",
"socket.io-redis": "^0.1.4",
"tweetnacl-util": "^0.15.0",
"winston": "~1.0.0"
},
"scripts": {
......
......@@ -31,8 +31,6 @@ module.exports = function (grunt) {
'assets/app/bower_components/ngImgCrop/compile/minified/ng-img-crop.js',
'assets/app/bower_components/bootstrap-filestyle/src/bootstrap-filestyle.min.js',
'assets/app/bower_components/ngMask/dist/ngMask.min.js',
'assets/app/bower_components/fast-sha256/sha256.min.js',
'assets/app/bower_components/tweetnacl-util/nacl-util.min.js',
'assets/scripts/lib/sails.io.js'
];
......@@ -69,6 +67,7 @@ module.exports = function (grunt) {
'assets/scripts/modules/student/controllers/pictoexp.js',
'assets/scripts/modules/student/controllers/newscene.js',
'assets/scripts/modules/student/controllers/instructiondetail.js',
'assets/scripts/modules/student/controllers/tpvmodal.js',
'assets/scripts/modules/translate/controllers/translate.js',
'assets/scripts/services/services.js',
......
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