From 61e5bbdc7f4311dbdad61a0267e559e5ffef430b Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 25 Aug 2015 11:22:31 +0200 Subject: [PATCH 01/28] Change the way dashboard is retrieve from the server Instead of 3 asynchronous http#Get, there is now only one http#Get /dashboard and it's the server that build a response containing three column --- backend/src/Backend.ts | 11 +- backend/src/Server.ts | 54 +++- backend/src/api/BadgeRouter.ts | 14 +- backend/src/api/DashboardRouter.ts | 227 +++++++++++++++ backend/src/api/GoalDefinitionRouter.ts | 4 + backend/src/api/GoalInstanceRouter.ts | 68 +++-- backend/src/api/RouterItf.ts | 11 +- backend/src/api/UserRouter.ts | 12 + .../user/BadgeIDsToNumberOfTimesEarnedMap.ts | 10 + backend/src/user/Entity.ts | 129 +++++++++ backend/src/user/Team.ts | 5 + backend/src/user/User.ts | 145 +--------- backend/src/user/UserRepository.ts | 12 +- frontend/app/index.html | 2 + frontend/app/scripts/app.js | 7 +- .../app/scripts/controllers/ServiceBadgeV2.js | 64 +++-- .../scripts/controllers/ServiceChallenge.js | 26 +- .../scripts/controllers/ServiceDashboard.js | 25 ++ .../app/scripts/controllers/ServiceGoal.js | 78 ++--- .../app/scripts/controllers/ServiceSensor.js | 7 +- frontend/app/scripts/controllers/dashboard.js | 73 +++++ frontend/app/scripts/controllers/goal.js | 18 +- frontend/app/scripts/controllers/home.js | 272 +++++++++--------- frontend/app/views/dashboard.html | 7 + .../app/views/homepage/list-challenge.html | 4 +- 25 files changed, 869 insertions(+), 416 deletions(-) create mode 100644 backend/src/api/DashboardRouter.ts create mode 100644 backend/src/api/UserRouter.ts create mode 100644 backend/src/user/BadgeIDsToNumberOfTimesEarnedMap.ts create mode 100644 backend/src/user/Entity.ts create mode 100644 backend/src/user/Team.ts create mode 100644 frontend/app/scripts/controllers/ServiceDashboard.js create mode 100644 frontend/app/scripts/controllers/dashboard.js create mode 100644 frontend/app/views/dashboard.html diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index c049ca4..d7e9e79 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -6,6 +6,7 @@ import Server = require('./Server'); import BadgeRouter = require('./api/BadgeRouter'); import GoalDefinitionRouter = require('./api/GoalDefinitionRouter'); import GoalInstanceRouter = require('./api/GoalInstanceRouter'); +import DashboardRouter = require('./api/DashboardRouter'); import BadgeRepository = require('./badge/BadgeRepository'); import BadgeFactory = require('./badge/BadgeFactory'); @@ -26,6 +27,7 @@ import OverallGoalCondition = require('./condition/OverallGoalCondition'); import TimeBox = require('./TimeBox'); import StoringHandler = require('./StoringHandler'); +import Middleware = require('./Middleware'); class Backend extends Server { @@ -50,7 +52,9 @@ class Backend extends Server { * @param {Array} arguments - Server's command line arguments. */ constructor(listeningPort:number, arguments:Array) { - super(listeningPort, arguments); + this.userRepository = new UserRepository(); + + super(listeningPort, arguments, this.userRepository); this.badgeRepository = new BadgeRepository(); this.badgeFactory = new BadgeFactory(); @@ -77,8 +81,11 @@ class Backend extends Server { */ buildAPI() { var self = this; + var loginCheck = super.requireLogin; + + this.app.use('/dashboard', (new DashboardRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository,self.badgeRepository, new Middleware())).getRouter()); - this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository)).getRouter()); + this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.goalInstanceRepository, self.userRepository)).getRouter()); this.app.use("/challenges", (new GoalInstanceRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); diff --git a/backend/src/Server.ts b/backend/src/Server.ts index 9a214bc..ee25530 100644 --- a/backend/src/Server.ts +++ b/backend/src/Server.ts @@ -7,11 +7,9 @@ var http:any = require("http"); var express:any = require("express"); var bodyParser:any = require("body-parser"); +var session = require('client-sessions'); -import Badge = require('./badge/Badge'); -import Operand = require('./condition/expression/Operand'); -import GoalCondition = require('./condition/Condition'); -import TimeBox = require('./TimeBox'); +import UserRepository = require('./user/UserRepository'); /** * Represents a Server managing Namespaces. @@ -44,15 +42,18 @@ class Server { */ httpServer:any; + userRepository:UserRepository; + /** * Constructor. * * @param {number} listeningPort - Listening port. * @param {Array} arguments - Command line arguments. */ - constructor(listeningPort:number, arguments:Array) { + constructor(listeningPort:number, arguments:Array, userRepository:UserRepository) { this.listeningPort = listeningPort; this._buildServer(); + this.userRepository = userRepository; } /** @@ -66,15 +67,58 @@ class Server { this.app.use(bodyParser.json()); // for parsing application/json this.app.use(bodyParser.urlencoded({extended: true})); // for parsing application/x-www-form-urlencoded + // Handle client session with mozilla library + this.app.use(session({ + cookieName: 'session', + secret: 'random_string_goes_here', // TODO : make secret field a high-entropy string instead of this bullshit + duration: 30 * 60 * 1000, + activeDuration: 5 * 60 * 1000, + })); + this.app.use(function (req, res, next) { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.header("Access-Control-Allow-Methods", "POST, GET, DELETE"); next(); }); + + this.app.use('/login', function (req, res, next) { + if (req.session && req.session.user) { + this.userRepository.userExists(req.session.user.id, + function (user) { + if (user) { + req.user = user; + delete req.user.password; // delete the password from the session + req.session.user = user; //refresh the session value + res.locals.user = user; + } + // finishing processing the middleware and run the route + next(); + }, + function (err) { + console.log("PROBLEME"); + res.send(err); + }); + } else { + next(); + } + + }); + + this.httpServer = http.createServer(this.app); } + requireLogin(req, res, next) { + if (!req.user) { + var route = req.get('host')+'/login'; + console.log('redirection vers', route); + res.redirect(route); + } else { + next(); + } + } + /** * Runs the Server. * diff --git a/backend/src/api/BadgeRouter.ts b/backend/src/api/BadgeRouter.ts index 2caad8e..fd69033 100644 --- a/backend/src/api/BadgeRouter.ts +++ b/backend/src/api/BadgeRouter.ts @@ -30,8 +30,9 @@ class BadgeRouter extends RouterItf { * @param badgeRepository * The badge repository to save and retrieve badges */ - constructor(badgeRepository:BadgeRepository, badgeFactory:BadgeFactory, userRepository:UserRepository) { - super(); + constructor(badgeRepository:BadgeRepository, badgeFactory:BadgeFactory, userRepository:UserRepository, loginCheck) { + console.log("LOGIN CHECK", loginCheck); + super(loginCheck); if(!badgeRepository) { throw new BadArgumentException('Badge repository is null'); @@ -53,6 +54,15 @@ class BadgeRouter extends RouterItf { buildRouter() { var self = this; + var loginCheckFunc = self.loginCheckFunc; + + console.log("LOOOL", loginCheckFunc); + + this.router.post('/lol', function(req,res) { + console.log("YEAH MAGUEULE"); + res.send('LOOOL'); + }); + this.router.get('/trophyWall', function(req,res) { self.getAllFinishedBadges(req,res); }); diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts new file mode 100644 index 0000000..5b0453d --- /dev/null +++ b/backend/src/api/DashboardRouter.ts @@ -0,0 +1,227 @@ +import RouterItf = require('./RouterItf'); + +import ChallengeRepository = require('../challenge/ChallengeRepository'); +import ChallengeFactory = require('../challenge/ChallengeFactory'); +import GoalRepository = require('../goal/GoalRepository'); +import BadgeRepository = require('../badge/BadgeRepository'); +import UserRepository = require('../user/UserRepository'); +import Challenge = require('../challenge/Challenge'); +import Clock = require('../Clock'); +import ChallengeStatus = require('../Status'); +import Goal = require('../goal/Goal'); + +import Middleware = require('../Middleware'); + +class DashboardRouter extends RouterItf { + public static DEMO:boolean = true; + private jsonStub:any = {}; + public static STUB_FILE:string = './stub_values.json'; + + private challengeRepository:ChallengeRepository; + private challengeFactory:ChallengeFactory; + + private goalRepository:GoalRepository; + + private userRepository:UserRepository; + + private badgeRepository:BadgeRepository; + + private middleware:Middleware; + + constructor(challengeRepository:ChallengeRepository, challengeFactory:ChallengeFactory, goalRepository:GoalRepository, userRepository:UserRepository, badgeRepository:BadgeRepository, middleware:Middleware) { + super(); + + var fs = require('fs'); + var data = fs.readFileSync(DashboardRouter.STUB_FILE, "utf-8"); + this.jsonStub = JSON.parse(data); + + this.challengeRepository = challengeRepository; + this.challengeFactory = challengeFactory; + this.goalRepository = goalRepository; + this.userRepository = userRepository; + this.badgeRepository = badgeRepository; + this.middleware = middleware; + } + + buildRouter() { + var self = this; + + this.router.get('/', function (req, res) { + self.getDashboard(req, res); + }); + } + + getDashboard(req, res) { + console.log("GETTING DASHBOARD"); + var result:any = {}; + + // try { + // First col : available goal + var descriptionOfAvailableGoals = this.goalRepository.getListOfUntakedGoalInJSONFormat(this.userRepository.getCurrentUser(), this.challengeRepository); + + // Second col : badge description + var descriptionOfBadges:any[] = []; + + var badges = this.userRepository.getCurrentUser().getFinishedBadges(); + for (var currentBadgeIDIndex in badges) { + var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); + var dataTrophy = { + number: badges[currentBadgeIDIndex], + badge: currentBadge + }; + + descriptionOfBadges.push(dataTrophy); + } + + // Third col : Evaluate challenge and return them + var descriptionOfChallenges:any[] = []; + + var challenges = this.userRepository.getCurrentUser().getChallenges(); + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + + this.evaluateChallenge(currentChallenge, currentChallengeID); + + var currentChallengeDesc = currentChallenge.getDataInJSON(); + descriptionOfChallenges.push(currentChallengeDesc); + } + + result.goals = descriptionOfAvailableGoals; + result.badges = descriptionOfBadges; + result.challenges = descriptionOfChallenges; + + res.send({success: 'Everything is fine', data: result}); + /* + } + catch (e) { + res.send({error: e.toString()}); + } + */ + } + + private evaluateChallenge(challengeToEvaluate:Challenge, challengeID) { + var self = this; + + if (!DashboardRouter.DEMO) { + + //TODO move what follow + var required = challengeToEvaluate.getSensors(); + + var requiredSensorName = Object.keys(required); + var numberToLoad:number = requiredSensorName.length; + + for (var currentSensorName in requiredSensorName) { + (function (currentSensorName) { + var startDate:string = '' + required[currentSensorName].startDate; + var endDate:string = '' + required[currentSensorName].endDate; + + var path = 'http://smartcampus.unice.fr/sensors/' + currentSensorName + '/data?date=' + startDate + '/' + endDate; + var dataJsonString = ""; + + this.middleware.getSensorsInfo(required, numberToLoad, dataJsonString, path, + function () { + var result = challengeToEvaluate.evaluate(required); + if (result) { + var newChall = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); + if (newChall != null) { + self.evaluateChallenge(newChall, newChall.getId()); + } + } + console.log("All data were retrieve properly"); + return challengeToEvaluate.getProgress(); + }, + function () { + return {error: "Error occurred in middleware"}; + }); + + })(requiredSensorName[currentSensorName]); + } + } + else { + console.log('++++++++++++++++++++++ \tMODE DEMO\t+++++++++++++++++++++'); + + if (challengeToEvaluate.haveToStart(Clock.getCurrentDemoMoment())) { + challengeToEvaluate.setStatus(ChallengeStatus.RUN); + } + + var result = challengeToEvaluate.evaluate(this.jsonStub); + + // Check if the challenge is achieved and finished + if (result && challengeToEvaluate.isFinished()) { + //console.log("Le challenge est réussi et terminé"); + + // Add finished badge to current user + this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); + + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateChallenge(newChallenge, newChallenge.getId()); + } + } + + // Check if the challenge is not achieved but finished + else if (!result && challengeToEvaluate.isFinished()) { + //console.log("Le challenge est FAIL et terminé"); + + var user = this.userRepository.getCurrentUser(); + user.deleteChallenge(challengeToEvaluate.getId()); + + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateChallenge(newChallenge, newChallenge.getId()); + } + } + + return challengeToEvaluate.getProgress(); + } + } + + // debug only + private addFinishedBadge(challengeID:string, userID:string) { + /* + console.log('add finished badge'); + console.log('user id : ', userID); + console.log('challenge ID : ', challengeID); + */ + var user = this.userRepository.getUser(userID); + var badgeID = this.challengeRepository.getBadgeByChallengeID(challengeID); + user.addFinishedBadge(badgeID); + user.deleteChallenge(challengeID); + } + + createGoalInstance(goalID:string, date:moment.Moment):Challenge { + // TODO ! stub ! + // The data object below is a stub to manually + // bind a symbolic name to a sensor name. + // In the future, this won't be hardcoded but + // will be set by final user during the account + // creation process + + var data = + { + "goal": { + "id": goalID, + "conditions": {"TMP_CLI": "TEMP_443V"} + } + }; + + var goal:Goal = this.goalRepository.getGoal(goalID); + + //console.log("Je construit un challenge en partant du principe que nous sommes le ", date.toISOString()); + var goalInstance = this.challengeFactory.createGoalInstance(data, this.goalRepository, null, date); + + if (goalInstance.getEndDate().isAfter(goal.getEndDate())) { + return null; + } + + this.challengeRepository.addGoalInstance(goalInstance); + this.userRepository.getCurrentUser().addChallenge(goalInstance.getId()); + return goalInstance; + } +} + +export = DashboardRouter; \ No newline at end of file diff --git a/backend/src/api/GoalDefinitionRouter.ts b/backend/src/api/GoalDefinitionRouter.ts index 772e3c5..3c6db6d 100644 --- a/backend/src/api/GoalDefinitionRouter.ts +++ b/backend/src/api/GoalDefinitionRouter.ts @@ -54,6 +54,10 @@ class GoalDefinitionRouter extends RouterItf { res.send(result); } + /** + * --------------------- + */ + /** * This method will return all goals definition * using goalDefinitionRepository#getListOfGoalsInJsonFormat diff --git a/backend/src/api/GoalInstanceRouter.ts b/backend/src/api/GoalInstanceRouter.ts index d2dc718..6b07e49 100644 --- a/backend/src/api/GoalInstanceRouter.ts +++ b/backend/src/api/GoalInstanceRouter.ts @@ -11,12 +11,12 @@ import ChallengeRepository = require('../challenge/ChallengeRepository'); import ChallengeFactory = require('../challenge/ChallengeFactory'); import GoalRepository = require('../goal/GoalRepository'); import Challenge = require('../challenge/Challenge'); -import ChallengeStatus = require('../Status'); import UserRepository = require('../user/UserRepository'); import Goal = require('../goal/Goal'); import Middleware = require('../Middleware'); import Clock = require('../Clock'); +import ChallengeStatus = require('../Status'); import BadRequestException = require('../exceptions/BadRequestException'); @@ -165,6 +165,9 @@ class GoalInstanceRouter extends RouterItf { return goalInstance; } + /** + * --------------------- + */ getAllChallenges(req:any, res:any) { var result:any[] = []; @@ -206,11 +209,15 @@ class GoalInstanceRouter extends RouterItf { res.send(this.evaluateChallenge(goalInstanceToEvaluate, goalInstanceID)); } + + /** + * --------------------- + */ evaluateAll(req:any, res:any) { try { var challenges = this.userRepository.getCurrentUser().getChallenges(); - for (var challenge in challenges) { - var currentChallengeID = challenges[challenge]; + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; var challengeToEvaluate = this.goalInstanceRepository.getGoalInstance(currentChallengeID); this.evaluateChallenge(challengeToEvaluate, currentChallengeID); } @@ -234,13 +241,13 @@ class GoalInstanceRouter extends RouterItf { user.deleteChallenge(challengeID); } - private evaluateChallenge(goalInstanceToEvaluate:Challenge, goalInstanceID) { + private evaluateChallenge(challengeToEvaluate:Challenge, challengeID) { var self = this; if (!GoalInstanceRouter.DEMO) { //TODO move what follow - var required = goalInstanceToEvaluate.getSensors(); + var required = challengeToEvaluate.getSensors(); var requiredSensorName = Object.keys(required); var numberToLoad:number = requiredSensorName.length; @@ -255,19 +262,19 @@ class GoalInstanceRouter extends RouterItf { this.middleware.getSensorsInfo(required, numberToLoad, dataJsonString, path, function () { - var result = goalInstanceToEvaluate.evaluate(required); + var result = challengeToEvaluate.evaluate(required); if (result) { - var newChall = self.createGoalInstance(goalInstanceToEvaluate.getGoalDefinition().getUUID(), goalInstanceToEvaluate.getEndDate()); - this.addFinishedBadge(goalInstanceID, this.userRepository.getCurrentUser().getUUID()); + var newChall = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); if (newChall != null) { self.evaluateChallenge(newChall, newChall.getId()); } } console.log("All data were retrieve properly"); - return goalInstanceToEvaluate.getProgress(); + return challengeToEvaluate.getProgress(); }, function () { - return {error: "Error occured in middleware"}; + return {error: "Error occurred in middleware"}; }); })(requiredSensorName[currentSensorName]); @@ -276,34 +283,41 @@ class GoalInstanceRouter extends RouterItf { else { console.log('++++++++++++++++++++++ \tMODE DEMO\t+++++++++++++++++++++'); - if(goalInstanceToEvaluate.haveToStart(Clock.getCurrentDemoMoment())) { - goalInstanceToEvaluate.setStatus(ChallengeStatus.RUN); + if(challengeToEvaluate.haveToStart(Clock.getCurrentDemoMoment())) { + challengeToEvaluate.setStatus(ChallengeStatus.RUN); } - var result = goalInstanceToEvaluate.evaluate(this.jsonStub); - if (result && goalInstanceToEvaluate.isFinished()) { + var result = challengeToEvaluate.evaluate(this.jsonStub); + + // Check if the challenge is achieved and finished + if (result && challengeToEvaluate.isFinished()) { //console.log("Le challenge est réussi et terminé"); - var newChall = self.createGoalInstance(goalInstanceToEvaluate.getGoalDefinition().getUUID(), goalInstanceToEvaluate.getEndDate()); - this.addFinishedBadge(goalInstanceID, this.userRepository.getCurrentUser().getUUID()); - if (newChall != null) { - self.evaluateChallenge(newChall, newChall.getId()); + + // Add finished badge to current user + this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); + + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateChallenge(newChallenge, newChallenge.getId()); } } - else if (!result && goalInstanceToEvaluate.isFinished()) { - console.log("Le challenge est FAIL et terminé"); - var newChall = self.createGoalInstance(goalInstanceToEvaluate.getGoalDefinition().getUUID(), goalInstanceToEvaluate.getEndDate()); + + // Check if the challenge is not achieved but finished + else if (!result && challengeToEvaluate.isFinished()) { + //console.log("Le challenge est FAIL et terminé"); var user = this.userRepository.getCurrentUser(); - user.deleteChallenge(goalInstanceToEvaluate.getId()); + user.deleteChallenge(challengeToEvaluate.getId()); - if (newChall != null) { - self.evaluateChallenge(newChall, newChall.getId()); + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateChallenge(newChallenge, newChallenge.getId()); } } - - - return goalInstanceToEvaluate.getProgress(); + return challengeToEvaluate.getProgress(); } } } diff --git a/backend/src/api/RouterItf.ts b/backend/src/api/RouterItf.ts index a7799d8..830dfd6 100644 --- a/backend/src/api/RouterItf.ts +++ b/backend/src/api/RouterItf.ts @@ -3,7 +3,7 @@ */ /// -var express : any = require("express"); +var express:any = require("express"); /** * Router Interface @@ -12,18 +12,21 @@ var express : any = require("express"); */ class RouterItf { + protected loginCheckFunc; + /** * Router property. * * @property router * @type any */ - router : any; + router:any; /** * Constructor. */ - constructor() { + constructor(loginCheck = null) { + this.loginCheckFunc = loginCheck; this.router = express.Router(); // middleware specific to this router @@ -52,6 +55,8 @@ class RouterItf { buildRouter() { //Logger.warn("RouterItf - buildRouter : Method need to be implemented."); } + + } export = RouterItf; \ No newline at end of file diff --git a/backend/src/api/UserRouter.ts b/backend/src/api/UserRouter.ts new file mode 100644 index 0000000..e202d6b --- /dev/null +++ b/backend/src/api/UserRouter.ts @@ -0,0 +1,12 @@ +import RouterItf = require('./RouterItf'); + +class UserRouter extends RouterItf { + constructor() { + super(); + } + + buildRouter() { + } +} + +export = UserRouter; \ No newline at end of file diff --git a/backend/src/user/BadgeIDsToNumberOfTimesEarnedMap.ts b/backend/src/user/BadgeIDsToNumberOfTimesEarnedMap.ts new file mode 100644 index 0000000..96abc51 --- /dev/null +++ b/backend/src/user/BadgeIDsToNumberOfTimesEarnedMap.ts @@ -0,0 +1,10 @@ +/** + * Map of finished badges
+ * key : badgeID, + * associated : number of times that you earned this badge + */ +interface BadgeIDsToNumberOfTimesEarnedMap { + [idBadge:number]: number; +} + +export = BadgeIDsToNumberOfTimesEarnedMap; \ No newline at end of file diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts new file mode 100644 index 0000000..221f88f --- /dev/null +++ b/backend/src/user/Entity.ts @@ -0,0 +1,129 @@ +import uuid = require('node-uuid'); + +import Goal = require('../goal/Goal'); +import Challenge = require('../challenge/Challenge'); +import Badge = require('../badge/Badge'); +import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); + +import BadArgumentException = require('../exceptions/BadArgumentException'); + +class Entity { + + private id; + private name:string; + private currentChallenges:string[] = []; + private finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; + + constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}) { + + this.id = (id) ? id : uuid.v4(); + + this.name = name; + this.currentChallenges = currentChallenges; + this.finishedBadgesMap = finishedBadgesMap; + } + + getCurrentChallenges():string [] { + return this.currentChallenges; + } + + public getUUID() { + return this.id; + } + + public hasUUID(aUUID:string):boolean { + return this.id === aUUID; + } + + public setUUID(aUUID:string):void { + this.id = aUUID; + } + + public getName():string { + return this.name; + } + + public hasName(name:string):boolean { + return this.getName() === name; + } + + public setName(name:string):void { + this.name = name; + } + + public addChallenge(challengeID:string):void { + if (!challengeID) { + throw new Error('Can not add a new goal to user ' + this.getName() + ' given goal is null'); + } + + this.currentChallenges.push(challengeID); + } + + public deleteChallenge(challengeID:string):void { + + var challengeIndex:number = this.getChallenge(challengeID); + if (challengeIndex == -1) { + throw new BadArgumentException('Can not find given challenge ID'); + } + else { + this.currentChallenges.splice(challengeIndex, 1); + } + + console.log("Challenge deleted ! Current challenges:", this.currentChallenges); + } + + public getChallenge(challengeID:string):number { + var result:number = -1; + + for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { + if (this.currentChallenges[currentChallengeIndex] === challengeID) { + result = currentChallengeIndex; + } + } + + return result; + } + + public getChallenges():string[] { + return this.currentChallenges; + } + + public setChallenges(challenges:string[]):void { + this.currentChallenges = challenges; + } + + public getFinishedBadges():BadgeIDsToNumberOfTimesEarnedMap { + return this.finishedBadgesMap; + } + + public getFinishedBadgesID():string[] { + return Object.keys(this.finishedBadgesMap); + } + + public setFinishedBadges(finishedBadges:BadgeIDsToNumberOfTimesEarnedMap) { + this.finishedBadgesMap = finishedBadges; + } + + public addFinishedBadge(badgeID:string) { + if (!badgeID) { + throw new BadArgumentException('Can not add given badge to user' + this.getName() + '. Badge given is null'); + } + + if (this.finishedBadgesMap.hasOwnProperty(badgeID)) { + this.finishedBadgesMap[badgeID]++; + } else { + this.finishedBadgesMap[badgeID] = 1; + } + } + + public getDataInJSON():any { + return { + id: this.id, + name: this.name, + currentChallenges: this.currentChallenges, + finishedBadgesMap: this.finishedBadgesMap + } + } +} + +export = Entity; \ No newline at end of file diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts new file mode 100644 index 0000000..848e243 --- /dev/null +++ b/backend/src/user/Team.ts @@ -0,0 +1,5 @@ +class Team { + +} + +export = Team; \ No newline at end of file diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index e0c8a2c..018e515 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -1,146 +1,11 @@ -/// -import uuid = require('node-uuid'); +import Entity = require('./Entity'); +import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); -import Goal = require('../goal/Goal'); -import Challenge = require('../challenge/Challenge'); -import Badge = require('../badge/Badge'); - -import BadArgumentException = require('../exceptions/BadArgumentException'); - -/** - * Map of finished badges
- * key : badgeID, - * associated : number of times that you earned this badge - */ -interface BadgeIDsToNumberOfTimesEarnedMap { - [idBadge:number]: number; -} - - -class User { - - private id; - private name:string; - private currentChallenges:string[] = []; - private finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; +class User extends Entity { constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}) { - - this.id = (id) ? id : uuid.v4(); - - this.name = name; - this.currentChallenges = currentChallenges; - this.finishedBadgesMap = finishedBadgesMap; - } - - getCurrentChallenges():string [] { - return this.currentChallenges; - } - - public getUUID() { - return this.id; - } - - public hasUUID(aUUID:string):boolean { - return this.id === aUUID; - } - - public setUUID(aUUID:string):void { - this.id = aUUID; - } - - public getName():string { - return this.name; - } - - public hasName(name:string):boolean { - return this.getName() === name; - } - - public setName(name:string):void { - this.name = name; - } - - public addChallenge(challengeID:string):void { - if (!challengeID) { - throw new Error('Can not add a new goal to user ' + this.getName() + ' given goal is null'); - } - - this.currentChallenges.push(challengeID); - } - - public deleteChallenge(challengeID:string):void { - - var challengeIndex:number = this.getChallenge(challengeID); - if (challengeIndex == -1) { - throw new BadArgumentException('Can not find given challenge ID'); - } - else { - this.currentChallenges.splice(challengeIndex, 1); - } - - console.log("Challenge deleted ! Current challenges:", this.currentChallenges); - } - - /** - * This method will return the index of the given - * challenge id - * @param challengeID - * @returns {number} - * Index of given challenge ID - */ - public getChallenge(challengeID:string):number { - var result:number = -1; - - for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { - if (this.currentChallenges[currentChallengeIndex] === challengeID) { - result = currentChallengeIndex; - } - } - - return result; - } - - public getChallenges():string[] { - return this.currentChallenges; - } - - public setChallenges(challenges:string[]):void { - this.currentChallenges = challenges; - } - - public getFinishedBadges():BadgeIDsToNumberOfTimesEarnedMap { - return this.finishedBadgesMap; - } - - public setFinishedBadges(finishedBadges:BadgeIDsToNumberOfTimesEarnedMap) { - this.finishedBadgesMap = finishedBadges; - } - - public getFinishedBadgesID():string[] { - return Object.keys(this.finishedBadgesMap); - } - - public addFinishedBadge(badgeID:string) { - if (!badgeID) { - throw new BadArgumentException('Can not add given badge to user' + this.getName() + '. Badge given is null'); - } - - if (this.finishedBadgesMap.hasOwnProperty(badgeID)) { - this.finishedBadgesMap[badgeID]++; - } else { - this.finishedBadgesMap[badgeID] = 1; - } - } - - public getDataInJSON():any { - return { - id: this.id, - name: this.name, - currentChallenges: this.currentChallenges, - finishedBadgesMap: this.finishedBadgesMap - } + super(name, id, currentChallenges, finishedBadgesMap); } } -export = User; +export = User; \ No newline at end of file diff --git a/backend/src/user/UserRepository.ts b/backend/src/user/UserRepository.ts index 5664836..a0fd4e6 100644 --- a/backend/src/user/UserRepository.ts +++ b/backend/src/user/UserRepository.ts @@ -6,6 +6,12 @@ class UserRepository { private currentUser:User; + userExists(userID:string, successCallBack:Function, failCallBack:Function) { + var user:User = this.getUser(userID); + + (user != null) ? successCallBack(user) : failCallBack('User not found'); + } + public addUser(user:User) { this.users.push(user); } @@ -32,7 +38,7 @@ class UserRepository { public getDataInJSON():any { var result:any[] = []; - for(var currentUserIndex in this.users) { + for (var currentUserIndex in this.users) { var currentUser = this.users[currentUserIndex]; result.push(currentUser.getDataInJSON()); } @@ -43,9 +49,9 @@ class UserRepository { public displayShortState() { console.log("\n\n+++\t Etat du repository des utilisateurs\t+++"); - for(var currentUserIndex in this.users) { + for (var currentUserIndex in this.users) { var currentUser = this.users[currentUserIndex]; - console.log("#",currentUser.getUUID(),"\t |\tUser : '", currentUser.getName(), "'") + console.log("#", currentUser.getUUID(), "\t |\tUser : '", currentUser.getName(), "'") } } } diff --git a/frontend/app/index.html b/frontend/app/index.html index b125872..de3b7a3 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -105,12 +105,14 @@ + + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 5fac02b..3115d3a 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -47,6 +47,11 @@ var app = angular templateUrl: 'views/create-badge-perso.html', controller: 'BadgeCtrlV2' }) + .when('/dashboard', { + templateUrl: '../views/dashboard.html', + controller: 'DashboardCtrl', + controllerAs:'Dashboard' + }) .otherwise({ redirectTo: '/' }); @@ -61,4 +66,4 @@ app.filter('range', function() { } return input; }; -}); \ No newline at end of file +}); diff --git a/frontend/app/scripts/controllers/ServiceBadgeV2.js b/frontend/app/scripts/controllers/ServiceBadgeV2.js index acc6d8a..2d0b3f0 100644 --- a/frontend/app/scripts/controllers/ServiceBadgeV2.js +++ b/frontend/app/scripts/controllers/ServiceBadgeV2.js @@ -4,38 +4,42 @@ * File with all the services associated to Badge (GET, POST) */ -var path = 'http://localhost:3000/badges/'; +var badgeBasePath = 'http://localhost:3000/badges/'; var app = angular.module('ecoknowledgeApp'); -app.service('ServiceBadgeV2',['$http', function ServiceBadgeV2($http){ - this.post = function(badge, successFunc, failFunc){ - $http.post(path+'new', badge) - .success(function(data){ - successFunc(data); - }) - .error(function(data) { - failFunc(data); - }); - }; +app.service('ServiceBadgeV2', ['$http', function ServiceBadgeV2($http) { + this.post = function (badge, successFunc, failFunc) { + $http.post(badgeBasePath + 'new', badge) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; - this.get = function(id, successFunc, failFunc){ - $http.get(path+id) - .success(function(data){ - successFunc(data); - }) - .error(function(data){ - failFunc(data); - }); - }; + this.get = function (id, successFunc, failFunc) { + var pathToGet = badgeBasePath + id; + console.log('Service Badge : Get on ', pathToGet); + $http.get(pathToGet) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; - this.getTrophies = function(successFunc, failFunc){ - $http.get(path+'trophyWall') - .success(function(data){ - successFunc(data); - }) - .error(function(data){ - failFunc(data); - }) - ; - }; + this.getTrophies = function (successFunc, failFunc) { + var pathToGet = badgeBasePath + 'trophyWall'; + console.log('Service Badge : Get on ', pathToGet); + $http.get(pathToGet) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }) + ; + }; }]); diff --git a/frontend/app/scripts/controllers/ServiceChallenge.js b/frontend/app/scripts/controllers/ServiceChallenge.js index 5a95095..40b5821 100644 --- a/frontend/app/scripts/controllers/ServiceChallenge.js +++ b/frontend/app/scripts/controllers/ServiceChallenge.js @@ -9,8 +9,10 @@ var basePath = 'http://localhost:3000/challenges/'; var app = angular.module('ecoknowledgeApp'); app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { this.get = function (id, successFunc, failFunc) { - console.log("BASEPATH", basePath); - $http.get(basePath + 'all/' + id) + var path = basePath + 'all/' + id; + console.log('Service Challenge : Get On ', path); + + $http.get(path) .success(function (data) { successFunc(data); }) @@ -18,6 +20,7 @@ app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { failFunc(data); }); }; + this.post = function (badge, successFunc, failFunc) { $http.post(basePath + 'new', badge) .success(function (data) { @@ -39,9 +42,10 @@ app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { }; this.evaluate = function (badgeName, successFunc, failFunc) { - var pathEval = basePath + 'evaluate/'+(badgeName==='all'?'all':('evaluatebadge?badgeName=' + badgeName)); - console.log('path eval trophy : ',pathEval); - $http.get(pathEval) + var path = basePath + 'evaluate/' + (badgeName === 'all' ? 'all' : ('evaluatebadge?badgeName=' + badgeName)); + console.log('Service Challenge : Get On ', path); + + $http.get(path) .success(function (data) { successFunc(data); }) @@ -50,16 +54,16 @@ app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { }); }; - this.delete = function(idGoal, successFunc, failFunc){ - console.log("DELETE : path", basePath+'delete/'+idGoal); + this.delete = function (idGoal, successFunc, failFunc) { + console.log('DELETE : path', basePath + 'delete/' + idGoal); var path = basePath + 'delete/' + idGoal; $http.delete(path) - .success(function(data){ - console.log("OK?3"); + .success(function (data) { + console.log('OK?3'); successFunc(data); }) - .error(function(data){ - console.log("FAIL MA KE PASSA?",data); + .error(function (data) { + console.log('FAIL MA KE PASSA?', data); failFunc(data); }); }; diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js new file mode 100644 index 0000000..5c139c5 --- /dev/null +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -0,0 +1,25 @@ +'use strict'; + +var path = 'http://localhost:3000/dashboard/'; + +var app = angular.module('ecoknowledgeApp'); + +app.service('ServiceDashboard', ['$http', function ServiceDashboard($http) { + this.get = function (successFunc, failFunc) { + console.log('Service dashboard : Get On ', path); + + $http.get(path) + .success(function (data) { + + + var goals = data.data.goals; + var badges = data.data.badges; + var challenges = data.data.challenges; + + successFunc(goals, badges, challenges); + }) + .error(function (data) { + failFunc(data); + }); + }; +}]); diff --git a/frontend/app/scripts/controllers/ServiceGoal.js b/frontend/app/scripts/controllers/ServiceGoal.js index 6e8ec7d..02aa5af 100644 --- a/frontend/app/scripts/controllers/ServiceGoal.js +++ b/frontend/app/scripts/controllers/ServiceGoal.js @@ -6,42 +6,46 @@ var app = angular.module('ecoknowledgeApp'); -app.service('ServiceGoal',['$http', function ServiceGoal($http){ - var basePathGoal = 'http://localhost:3000/goals/'; - this.get = function(id, successFunc, failFunc) { - var path = basePathGoal; - path += (id)? id : 'all'; - console.log('Get ON', path); - $http.get(path) - .success(function (data) { - successFunc(data); - }) - .error(function (data) { - console.log('path : ', path); - failFunc(data); - }); - }; - - this.getRequired= function(id, successFunc, failFunc) { - $http.get(basePathGoal+'required/?goalName=' + id) - .success(function (data) { - successFunc(data); - }) - .error(function (data) { - failFunc(data); - }); - }; - - this.post = function(goal, successFunc, failFunc){ - console.log('goal : ',goal); - console.log('path : ',basePathGoal+'new'); - $http.post(basePathGoal+'new', goal) - .success(function(){ - successFunc(); - }) - .error(function() { - failFunc(); - }); - }; +app.service('ServiceGoal', ['$http', function ServiceGoal($http) { + var basePathGoal = 'http://localhost:3000/goals/'; + this.get = function (id, successFunc, failFunc) { + var path = basePathGoal; + path += (id) ? id : 'all'; + console.log('Service Goal : Get On', path); + $http.get(path) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + console.log('path : ', path); + failFunc(data); + }); + }; + + this.getRequired = function (id, successFunc, failFunc) { + var path = basePathGoal + 'required/?goalName=' + id; + console.log('Service Goal : Get On', path); + + $http.get(basePathGoal + 'required/?goalName=' + id) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; + + this.post = function (goal, successFunc, failFunc) { + var path = basePathGoal + 'new'; + console.log('Service Goal : Get On', path); + + $http.post(path, goal) + .success(function () { + successFunc(); + }) + .error(function () { + failFunc(); + }); + }; }]); diff --git a/frontend/app/scripts/controllers/ServiceSensor.js b/frontend/app/scripts/controllers/ServiceSensor.js index 6800ee7..be79330 100644 --- a/frontend/app/scripts/controllers/ServiceSensor.js +++ b/frontend/app/scripts/controllers/ServiceSensor.js @@ -9,8 +9,9 @@ var app = angular.module('ecoknowledgeApp'); app.service('ServiceSensor',['$http', function ServiceSensor($http){ var path = 'http://localhost:3000/'; this.get = function(id, successFunc, failFunc) { - console.log(path+'sensors/' + id); - $http.get(path+'sensors/'+id) + var pathToGet = path + 'sensors/' + id; + console.log('service sensor : Get on', pathToGet); + $http.get(pathToGet) .success(function (data) { successFunc(data); }) @@ -19,4 +20,4 @@ app.service('ServiceSensor',['$http', function ServiceSensor($http){ }); }; -}]); \ No newline at end of file +}]); diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js new file mode 100644 index 0000000..402c53c --- /dev/null +++ b/frontend/app/scripts/controllers/dashboard.js @@ -0,0 +1,73 @@ +'use strict'; + +var app = angular.module('ecoknowledgeApp'); + +app.controller('DashboardCtrl', ['ServiceDashboard', function (ServiceDashboard) { + var self = this; + + self.goals = {}; + self.badges = {}; + self.challenges = {}; + + this.getDashboard = function () { + ServiceDashboard.get( + function (goals, badges, challenges) { + console.log('Result of dashboard : ', goals, badges, challenges); + + self.goals = goals; + self.badges = badges; + self.challenges = challenges; + }, + function (data) { + console.log('ERREUR MA GUEULE', data); + }); + }; + + this.getDashboard(); + }]); + + +app.directive('listGoal', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-goal.html', + controller: 'HomeCtrl', + controllerAs: 'homeCtrl' + }; +}); + +app.directive('listChallenge', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-challenge.html' + }; +}); + +app.directive('listTrophies', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-trophies.html' + }; +}); + + +app.directive('homepageChallenge', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-challenge.html' + }; +}); + +app.directive('homepageGoal', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-goal.html' + }; +}); + +app.directive('homepageTrophy', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-trophy.html' + }; +}); diff --git a/frontend/app/scripts/controllers/goal.js b/frontend/app/scripts/controllers/goal.js index 2833605..85528b3 100644 --- a/frontend/app/scripts/controllers/goal.js +++ b/frontend/app/scripts/controllers/goal.js @@ -39,32 +39,24 @@ var app = angular.module('ecoknowledgeApp') }; this.changeDayOfWeekFilter = function(iteration, dayOfWeekFilter) { - if(iteration.filter == null) { + if(iteration.filter === null) { iteration.filter = {}; } iteration.filter.dayOfWeekFilter = dayOfWeekFilter; }; - this.changePeriodOfDayFilter = function(iteration, periodOfDayFilter) { - if(iteration.filter == null) { - iteration.filter = {}; - } - iteration.filter.periodOfDayFilter = []; - }; - - this.togglePeriodOfDayFilter = function(iteration, periodOfDayFilter) { - if(iteration.filter == null) { + if(iteration.filter === null) { iteration.filter = {}; } - if(iteration.filter.periodOfDayFilter == null) { + if(iteration.filter.periodOfDayFilter === null) { iteration.filter.periodOfDayFilter = []; } var allIndex = iteration.filter.periodOfDayFilter.indexOf('all'); - if(allIndex > -1 && periodOfDayFilter != 'all') { + if(allIndex > -1 && periodOfDayFilter !== 'all') { iteration.filter.periodOfDayFilter.splice(allIndex,1); self.togglePeriodOfDayFilter(iteration, 'morning'); @@ -72,7 +64,7 @@ var app = angular.module('ecoknowledgeApp') self.togglePeriodOfDayFilter(iteration, 'night'); } - if(allIndex == -1 && periodOfDayFilter == 'all') { + if(allIndex === -1 && periodOfDayFilter === 'all') { iteration.filter.periodOfDayFilter = ['all']; return; } diff --git a/frontend/app/scripts/controllers/home.js b/frontend/app/scripts/controllers/home.js index 3b54e5c..75d9b69 100644 --- a/frontend/app/scripts/controllers/home.js +++ b/frontend/app/scripts/controllers/home.js @@ -8,152 +8,150 @@ * Controller of the ecoknowledgeApp for the firstpage which display * the objectives and the badges we have. */ -var app = angular.module('ecoknowledgeApp') - .controller('HomeCtrl', ['ServiceChallenge', 'ServiceGoal', 'ServiceBadgeV2','$window', function (ServiceChallenge, ServiceGoal, ServiceBadgeV2, $window) { - var self = this; - self.goals = []; - self.goalsInstance = []; - self.obtentionValue = {}; - self.trophies = []; - /* - * Add a goal the the array goals - */ - - self.takeGoal = function (goalID) { - - var toSend = {}; - toSend.id = goalID; - - console.log('TAKE GOAL', toSend); - ServiceChallenge.takeGoal(toSend, function (data) { - console.log('Objectif instancié ', data); - $window.location.reload(); - }, function (data) { - console.log('Fail sur l\'instanciation de l\'objectif', data); - }); - }; - - self.getGoals = function () { - self.goals = []; - ServiceGoal.get('', function (data) { - self.goals = data; - }, function () { - console.log('fail to get the goals'); - }); - }; - - self.getBadges = function () { - ServiceChallenge.evaluate('all',function(data){ - console.log('achieve eval'); - },function(data){ - console.log('fail eval'); - }); - - self.goalsInstance = []; - ServiceChallenge.get('', function (data) { - console.log('get the badges : '); - self.goalsInstance = data; - - for (var badgeIndex in self.goalsInstance) { - var currentBadge = self.goalsInstance[badgeIndex]; - var startDate = new Date(currentBadge.startDate); - var formattedStartDate = '' + startDate.getDate() + '/' + (startDate.getMonth() + 1) + '/' + startDate.getFullYear(); - - var endDate = new Date(currentBadge.endDate); - var formattedEndDate = '' + (endDate.getDate()) + '/' + (endDate.getMonth() + 1 )+ '/' + endDate.getFullYear(); - - currentBadge.startDate = formattedStartDate; - currentBadge.endDate = formattedEndDate; - } - console.log('goals : ', angular.toJson(self.goalsInstance)); - - }, function (data) { - console.debug('Fail to get the badges', data); - }); - }; - - self.addGoal = function (g) { - self.goals.push(g); - }; - - /* - * add a badge to the array badges - */ - self.addBadge = function (bdg) { - self.goalsInstance.add(bdg); - }; - - self.getTrophies = function(){ - self.trophies = []; - ServiceBadgeV2.getTrophies(function(data){ - self.trophies = data; - console.log('trophies get : ', self.trophies); - },function(data){ - console.log('Error getting trophies', data); - }); - }; - - self.removeObjective = function(objective){ - console.log("OBJECTIVE", objective); - - console.log('objective to remove : ',objective.id); - ServiceChallenge.delete(objective.id,function(data){ - console.log('Succeed to remove a goal instance : ', data); - var index = self.goalsInstance.indexOf(objective); - self.goalsInstance.splice(index, 1); - $window.location.reload(); - }, function(data){ - console.log('Failed to remove a goal',data); - }); - - }; - - self.getGoals(); - self.getBadges(); - self.getTrophies(); - }]); - -app.directive('listGoal', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/list-goal.html', - controller: 'HomeCtrl', - controllerAs: 'homeCtrl' +var app = angular.module('ecoknowledgeApp'); +app.controller('HomeCtrl', ['ServiceChallenge', 'ServiceGoal', 'ServiceBadgeV2', '$window', function (ServiceChallenge, ServiceGoal, ServiceBadgeV2, $window) { + var self = this; + self.goals = []; + self.challenges = []; + self.trophies = []; + /* + * Add a goal the the array goals + */ + + self.takeGoal = function (goalID) { + + var toSend = {}; + toSend.id = goalID; + + console.log('TAKE GOAL', toSend); + ServiceChallenge.takeGoal(toSend, function (data) { + console.log('Objectif instancié ', data); + $window.location.reload(); + }, function (data) { + console.log('Fail sur l\'instanciation de l\'objectif', data); + }); }; -}); -app.directive('listChallenge', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/list-challenge.html' + self.getGoals = function () { + self.goals = []; + ServiceGoal.get('', function (data) { + self.goals = data; + }, function () { + console.log('fail to get the goals'); + }); }; -}); -app.directive('listTrophies', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/list-trophies.html' + self.getBadges = function () { + ServiceChallenge.evaluate('all', function (data) { + console.log('achieve eval', data); + }, function (data) { + console.log('fail eval', data); + }); + + self.challenges = []; + ServiceChallenge.get('', function (data) { + console.log('get the badges : '); + self.challenges = data; + + for (var badgeIndex in self.challenges) { + var currentBadge = self.challenges[badgeIndex]; + var startDate = new Date(currentBadge.startDate); + var formattedStartDate = '' + startDate.getDate() + '/' + (startDate.getMonth() + 1) + '/' + startDate.getFullYear(); + + var endDate = new Date(currentBadge.endDate); + var formattedEndDate = '' + (endDate.getDate()) + '/' + (endDate.getMonth() + 1 ) + '/' + endDate.getFullYear(); + + currentBadge.startDate = formattedStartDate; + currentBadge.endDate = formattedEndDate; + } + console.log('goals : ', angular.toJson(self.challenges)); + + }, function (data) { + console.debug('Fail to get the badges', data); + }); }; -}); + self.addGoal = function (g) { + self.goals.push(g); + }; -app.directive('homepageChallenge', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/homepage-challenge.html' + /* + * add a badge to the array badges + */ + self.addBadge = function (bdg) { + self.challenges.add(bdg); }; -}); -app.directive('homepageGoal', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/homepage-goal.html' + self.getTrophies = function () { + self.trophies = []; + ServiceBadgeV2.getTrophies(function (data) { + self.trophies = data; + console.log('trophies get : ', self.trophies); + }, function (data) { + console.log('Error getting trophies', data); + }); }; -}); -app.directive('homepageTrophy', function () { - return { - restrict: 'E', - templateUrl: '../../views/homepage/homepage-trophy.html' + self.removeObjective = function (objective) { + console.log('objective to remove : ', objective.id); + ServiceChallenge.delete(objective.id, function (data) { + console.log('Succeed to remove a goal instance : ', data); + var index = self.challenges.indexOf(objective); + self.challenges.splice(index, 1); + $window.location.reload(); + }, function (data) { + console.log('Failed to remove a goal', data); + }); + }; -}); + + self.getGoals(); + self.getBadges(); + self.getTrophies(); +}]); +/* + app.directive('listGoal', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-goal.html', + controller: 'HomeCtrl', + controllerAs: 'homeCtrl' + }; + }); + + app.directive('listChallenge', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-challenge.html' + }; + }); + + app.directive('listTrophies', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/list-trophies.html' + }; + }); + + + app.directive('homepageChallenge', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-challenge.html' + }; + }); + + app.directive('homepageGoal', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-goal.html' + }; + }); + + app.directive('homepageTrophy', function () { + return { + restrict: 'E', + templateUrl: '../../views/homepage/homepage-trophy.html' + }; + }); + */ diff --git a/frontend/app/views/dashboard.html b/frontend/app/views/dashboard.html new file mode 100644 index 0000000..3b3b28e --- /dev/null +++ b/frontend/app/views/dashboard.html @@ -0,0 +1,7 @@ + +
+

EcoKnowledge

+ + + +
diff --git a/frontend/app/views/homepage/list-challenge.html b/frontend/app/views/homepage/list-challenge.html index 113fa22..d8903ae 100644 --- a/frontend/app/views/homepage/list-challenge.html +++ b/frontend/app/views/homepage/list-challenge.html @@ -1,9 +1,9 @@

Défis en cours

    -
  • +
-
Vous n'avez pas encore de défis en cours !
+
Vous n'avez pas encore de défis en cours !
From ba97718272ff67cae45ec59390688708bd274f93 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 25 Aug 2015 11:34:38 +0200 Subject: [PATCH 02/28] Dashboard view display rows correctly --- frontend/app/scripts/app.js | 2 +- frontend/app/scripts/controllers/dashboard.js | 43 ++++++++++--------- frontend/app/scripts/controllers/home.js | 3 +- .../app/views/homepage/homepage-goal.html | 2 +- .../app/views/homepage/list-challenge.html | 4 +- frontend/app/views/homepage/list-goal.html | 7 ++- .../app/views/homepage/list-trophies.html | 4 +- 7 files changed, 35 insertions(+), 30 deletions(-) diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 3115d3a..87328d9 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -50,7 +50,7 @@ var app = angular .when('/dashboard', { templateUrl: '../views/dashboard.html', controller: 'DashboardCtrl', - controllerAs:'Dashboard' + controllerAs:'dashboard' }) .otherwise({ redirectTo: '/' diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index 402c53c..de39987 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -3,36 +3,37 @@ var app = angular.module('ecoknowledgeApp'); app.controller('DashboardCtrl', ['ServiceDashboard', function (ServiceDashboard) { - var self = this; + var self = this; - self.goals = {}; - self.badges = {}; - self.challenges = {}; + self.goals = {}; + self.trophies = {}; + self.challenges = {}; - this.getDashboard = function () { - ServiceDashboard.get( - function (goals, badges, challenges) { - console.log('Result of dashboard : ', goals, badges, challenges); + this.getDashboard = function () { + console.log('on veut récupérer le dashboard!!'); + ServiceDashboard.get( + function (goals, badges, challenges) { + console.log('Result of dashboard : ', goals, badges, challenges); - self.goals = goals; - self.badges = badges; - self.challenges = challenges; - }, - function (data) { - console.log('ERREUR MA GUEULE', data); - }); - }; - - this.getDashboard(); - }]); + self.goals = goals; + self.trophies = badges; + self.challenges = challenges; + }, + function (data) { + console.log('ERREUR MA GUEULE', data); + }); + }; + console.log('Le fichier dshb est bien chargé'); + this.getDashboard(); +}]); app.directive('listGoal', function () { return { restrict: 'E', templateUrl: '../../views/homepage/list-goal.html', - controller: 'HomeCtrl', - controllerAs: 'homeCtrl' + controller: 'DashboardCtrl', + controllerAs: 'dashboard' }; }); diff --git a/frontend/app/scripts/controllers/home.js b/frontend/app/scripts/controllers/home.js index 75d9b69..280a5c1 100644 --- a/frontend/app/scripts/controllers/home.js +++ b/frontend/app/scripts/controllers/home.js @@ -104,10 +104,11 @@ app.controller('HomeCtrl', ['ServiceChallenge', 'ServiceGoal', 'ServiceBadgeV2', }); }; - +/* self.getGoals(); self.getBadges(); self.getTrophies(); +*/ }]); /* app.directive('listGoal', function () { diff --git a/frontend/app/views/homepage/homepage-goal.html b/frontend/app/views/homepage/homepage-goal.html index 7f8075b..7801e12 100644 --- a/frontend/app/views/homepage/homepage-goal.html +++ b/frontend/app/views/homepage/homepage-goal.html @@ -1,4 +1,4 @@ diff --git a/frontend/app/views/homepage/list-challenge.html b/frontend/app/views/homepage/list-challenge.html index d8903ae..6880071 100644 --- a/frontend/app/views/homepage/list-challenge.html +++ b/frontend/app/views/homepage/list-challenge.html @@ -1,9 +1,9 @@

Défis en cours

    -
  • +
-
Vous n'avez pas encore de défis en cours !
+
Vous n'avez pas encore de défis en cours !
diff --git a/frontend/app/views/homepage/list-goal.html b/frontend/app/views/homepage/list-goal.html index 4d5079c..76cc62c 100644 --- a/frontend/app/views/homepage/list-goal.html +++ b/frontend/app/views/homepage/list-goal.html @@ -1,9 +1,12 @@ +
+ {{dashboard.goals}} +

Objectifs disponibles

    -
  • +
-
Vous n'avez pas encore d'objectifs de disponible!
+
Vous n'avez pas encore d'objectifs de disponible!
diff --git a/frontend/app/views/homepage/list-trophies.html b/frontend/app/views/homepage/list-trophies.html index 6e5eae3..cfc6733 100644 --- a/frontend/app/views/homepage/list-trophies.html +++ b/frontend/app/views/homepage/list-trophies.html @@ -1,11 +1,11 @@

Tableau de chasse

    -
  • +
-
Vous n'avez pas encore de badge !
+
Vous n'avez pas encore de badge !
From b7fa60af18417fa129c3f50e442ebb42ed8c6ea5 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 25 Aug 2015 11:40:48 +0200 Subject: [PATCH 03/28] It is possible to take a goal from dashboard Add takeGoal route into dashboard router, add takeGoal function into dashboard.js controller and into ServiceDashboard --- backend/src/api/DashboardRouter.ts | 21 +++++++++++++++++++ .../scripts/controllers/ServiceDashboard.js | 17 ++++++++++++--- frontend/app/scripts/controllers/dashboard.js | 18 +++++++++++++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 5b0453d..853f729 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -49,6 +49,27 @@ class DashboardRouter extends RouterItf { this.router.get('/', function (req, res) { self.getDashboard(req, res); }); + + this.router.post('/takeGoal', function (req, res) { + self.newGoalInstance(req, res); + }); + } + + newGoalInstance(req:any, res:any) { + var goalID = req.body.id; + + if (!goalID) { + res.status(400).send({'error': 'goalID field is missing in request'}); + } + + var newChall:Challenge = this.createGoalInstance(goalID, Clock.getMoment(Clock.getNow())); + + if(newChall == null) { + res.send({'error': 'Can not take this challenge'}); + return; + } + + res.send({"success": ("Objectif ajouté !" + newChall.getDataInJSON())}); } getDashboard(req, res) { diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js index 5c139c5..77eb9b5 100644 --- a/frontend/app/scripts/controllers/ServiceDashboard.js +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -1,14 +1,14 @@ 'use strict'; -var path = 'http://localhost:3000/dashboard/'; +var dashboardBasePath = 'http://localhost:3000/dashboard/'; var app = angular.module('ecoknowledgeApp'); app.service('ServiceDashboard', ['$http', function ServiceDashboard($http) { this.get = function (successFunc, failFunc) { - console.log('Service dashboard : Get On ', path); + console.log('Service dashboard : Get On ', dashboardBasePath); - $http.get(path) + $http.get(dashboardBasePath) .success(function (data) { @@ -22,4 +22,15 @@ app.service('ServiceDashboard', ['$http', function ServiceDashboard($http) { failFunc(data); }); }; + + this.takeGoal = function (goalID, successFunc, failFunc) { + $http.post(dashboardBasePath + 'takeGoal', goalID) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; + }]); diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index de39987..bd6c89a 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -2,7 +2,7 @@ var app = angular.module('ecoknowledgeApp'); -app.controller('DashboardCtrl', ['ServiceDashboard', function (ServiceDashboard) { +app.controller('DashboardCtrl', ['ServiceDashboard', '$window', function (ServiceDashboard, $window) { var self = this; self.goals = {}; @@ -23,6 +23,22 @@ app.controller('DashboardCtrl', ['ServiceDashboard', function (ServiceDashboard) console.log('ERREUR MA GUEULE', data); }); }; + + self.takeGoal = function (goalID) { + var toSend = {}; + toSend.id = goalID; + + ServiceDashboard.takeGoal(toSend, + function (data) { + console.log('Objectif instancié ', data); + $window.location.reload(); + }, + function (data) { + console.log('Fail sur l\'instanciation de l\'objectif', data); + }); + }; + + console.log('Le fichier dshb est bien chargé'); this.getDashboard(); }]); From 37c76e333ed9efe65658192e478082c78b3ce80d Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 25 Aug 2015 14:29:25 +0200 Subject: [PATCH 04/28] Challenges can be deleted again --- backend/src/api/DashboardRouter.ts | 16 ++++++++++++++ backend/src/api/GoalInstanceRouter.ts | 2 ++ backend/src/condition/AverageOnValue.ts | 2 ++ backend/stub_values.json | 4 ++-- frontend/app/index.html | 4 ++-- .../scripts/controllers/ServiceDashboard.js | 21 ++++++++++++++++--- frontend/app/scripts/controllers/dashboard.js | 13 ++++++++++++ .../views/homepage/homepage-challenge.html | 4 ++-- frontend/app/views/homepage/list-goal.html | 3 --- 9 files changed, 57 insertions(+), 12 deletions(-) diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 853f729..ae2e7a8 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -50,6 +50,10 @@ class DashboardRouter extends RouterItf { self.getDashboard(req, res); }); + this.router.delete('/delete/:id', function (req, res) { + self.deleteChallenge(req, res); + }); + this.router.post('/takeGoal', function (req, res) { self.newGoalInstance(req, res); }); @@ -72,6 +76,18 @@ class DashboardRouter extends RouterItf { res.send({"success": ("Objectif ajouté !" + newChall.getDataInJSON())}); } + deleteChallenge(req:any, res:any) { + var goalID = req.params.id; + + try { + this.userRepository.getCurrentUser().deleteChallenge(goalID); + res.send({"success": "Objectif supprimé !"}); + } + catch (e) { + res.send({error: e.toString()}); + } + } + getDashboard(req, res) { console.log("GETTING DASHBOARD"); var result:any = {}; diff --git a/backend/src/api/GoalInstanceRouter.ts b/backend/src/api/GoalInstanceRouter.ts index 6b07e49..0f3ed8d 100644 --- a/backend/src/api/GoalInstanceRouter.ts +++ b/backend/src/api/GoalInstanceRouter.ts @@ -53,6 +53,7 @@ class GoalInstanceRouter extends RouterItf { buildRouter() { var self = this; + /* this.router.post('/new', function (req, res) { self.newGoalInstance(req, res); }); @@ -85,6 +86,7 @@ class GoalInstanceRouter extends RouterItf { this.router.post('/addBadge', function (req, res) { self.addFinishedBadge(req.challengeID, req.userID); }); + */ } diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index a6fd834..f5348bd 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -63,6 +63,8 @@ class AverageOnValue extends Condition { var remainingData:any = super.applyFilters(data); data = remainingData; + console.log('Remaining data', data); + var sensorNames:string[] = this.expression.getRequired(); var result = false; diff --git a/backend/stub_values.json b/backend/stub_values.json index cdd7b22..6713694 100644 --- a/backend/stub_values.json +++ b/backend/stub_values.json @@ -2,11 +2,11 @@ "TEMP_443V": { "values": [ { - "date": "1438346523000", + "date": "1440072603000", "value": 20 }, { - "date":"1438778523000", + "date":"1440072603000", "value":22 } ] diff --git a/frontend/app/index.html b/frontend/app/index.html index de3b7a3..c874cd7 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -62,13 +62,13 @@
- + + @@ -113,6 +114,7 @@ + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 87328d9..e12149e 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -52,6 +52,11 @@ var app = angular controller: 'DashboardCtrl', controllerAs:'dashboard' }) + .when('/login', { + templateUrl: '../views/login.html', + controller: 'LoginCtrl', + controllerAs:'loginCtrl' + }) .otherwise({ redirectTo: '/' }); diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js index dbbe4e0..487da29 100644 --- a/frontend/app/scripts/controllers/ServiceDashboard.js +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -18,6 +18,7 @@ app.service('ServiceDashboard', ['$http', function ServiceDashboard($http) { successFunc(data, goals, badges, challenges); }) .error(function (data) { + console.error('ServiceDashboard : fail get dashboard', data); failFunc(data); }); }; diff --git a/frontend/app/scripts/controllers/ServiceLogin.js b/frontend/app/scripts/controllers/ServiceLogin.js new file mode 100644 index 0000000..e834f47 --- /dev/null +++ b/frontend/app/scripts/controllers/ServiceLogin.js @@ -0,0 +1,21 @@ +'use strict'; + +var app = angular.module('ecoknowledgeApp'); + +var loginBasePath = 'http://localhost:3000/login/'; + +app.service('ServiceLogin', ['$http', function ServiceLogin($http) { + + this.login = function (name, successFunc, failFunc) { + var path = loginBasePath + ''; + console.log('ServiceLogin : login', path); + + $http.post(path, name) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; +}]); diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index c22c9f8..d6e8b79 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -2,7 +2,7 @@ var app = angular.module('ecoknowledgeApp'); -app.controller('DashboardCtrl', ['ServiceDashboard', '$window', function (ServiceDashboard, $window) { +app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', function (ServiceDashboard, $window, $location) { var self = this; self.goals = {}; @@ -13,7 +13,8 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', function (Servic self.request = {}; this.getDashboard = function () { - console.log('on veut récupérer le dashboard!!'); + console.log('Angular wanna get the dashboard'); + ServiceDashboard.get( function (data, goals, badges, challenges) { console.log('Result of dashboard : ', goals, badges, challenges); @@ -25,7 +26,8 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', function (Servic self.challenges = challenges; }, function (data) { - console.log('ERREUR MA GUEULE', data); + console.error("Redirection vers", data.redirectTo); + $location.path(data.redirectTo); }); }; @@ -59,7 +61,6 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', function (Servic }; - console.log('Le fichier dshb est bien chargé'); this.getDashboard(); }]); diff --git a/frontend/app/scripts/controllers/login.js b/frontend/app/scripts/controllers/login.js new file mode 100644 index 0000000..f53c956 --- /dev/null +++ b/frontend/app/scripts/controllers/login.js @@ -0,0 +1,25 @@ +'use strict'; + +var app = angular.module('ecoknowledgeApp'); + +app.controller('LoginCtrl', ['ServiceLogin', function (ServiceLogin) { + + var self = this; + + this.username = '' + this.password = ''; + this.debug = ''; + + this.connect = function () { + self.debug = 'ID:' + self.username + ', passwd:' + self.password; + ServiceLogin.login(self.username, + function (data) { + console.log('Login success: data received', data); + }, + function (data) { + console.log('Login fail: data received', data); + + }); + }; + +}]); diff --git a/frontend/app/styles/login.css b/frontend/app/styles/login.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index 13010c8..fd2a465 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -1,3 +1,8 @@ +.navbar { + margin-bottom:0px; + border : 0px solid white; +} + .browsehappy { margin: 0.2em 0; background: #ccc; diff --git a/frontend/app/views/login.html b/frontend/app/views/login.html new file mode 100644 index 0000000..b8750b1 --- /dev/null +++ b/frontend/app/views/login.html @@ -0,0 +1,27 @@ + + +
+ + + {{loginCtrl.username}} + {{loginCtrl.password}} + {{loginCtrl.debug}} +
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
From 81483be0971b1036800db7f35f8a53caae717517 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 27 Aug 2015 15:01:44 +0200 Subject: [PATCH 08/28] Add dashboard views Add team class implementation Add team repo Update and add tests related Add class Entity User have to login before being redirected to his dashboard Dashboard view have dropdown to change between personal and team 's views Each http#GET to the server use a token to identify itself to the serve and a key for the view wanted the token and the key are stored in a cookie (not in $rootScope -> refreshing the page will flush the $rootScope -> lol) Add login frontend controller and dashboard controller, all other controller are obsolete :a: The app has not the same features than before, for now :a: --- backend/db.json | 10 +- backend/src/Backend.ts | 25 +- backend/src/StoringHandler.ts | 14 + backend/src/api/DashboardRouter.ts | 282 ++++++++++++++---- backend/src/api/LoginRouter.ts | 41 +++ backend/src/goal/GoalRepository.ts | 4 +- backend/src/user/Team.ts | 67 ++++- backend/src/user/TeamFactory.ts | 31 ++ backend/src/user/TeamRepository.ts | 74 +++++ backend/src/user/UserFactory.ts | 3 +- backend/src/user/UserRepository.ts | 11 + backend/tests/TeamTest.ts | 60 ++++ .../integration/ChallengeBuildingTest.ts | 22 +- frontend/app/scripts/app.js | 28 ++ .../scripts/controllers/ServiceDashboard.js | 17 +- .../app/scripts/controllers/ServiceLogin.js | 20 +- frontend/app/scripts/controllers/dashboard.js | 34 ++- frontend/app/scripts/controllers/login.js | 16 +- frontend/app/views/dashboard.html | 26 +- frontend/app/views/homepage/homepage.html | 2 + frontend/app/views/login.html | 15 +- 21 files changed, 693 insertions(+), 109 deletions(-) create mode 100644 backend/src/api/LoginRouter.ts create mode 100644 backend/src/user/TeamFactory.ts create mode 100644 backend/src/user/TeamRepository.ts create mode 100644 backend/tests/TeamTest.ts diff --git a/backend/db.json b/backend/db.json index 3e966a3..dd91a65 100644 --- a/backend/db.json +++ b/backend/db.json @@ -103,7 +103,7 @@ "users": [ { "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", - "name": "Jackie!", + "name": "Charlie", "currentChallenges": [ ], "finishedBadgesMap": { @@ -112,6 +112,14 @@ } } ], + "teams": [ + { + "id" : "28aa8108-8830-4f43-abd1-3ab643303d92", + "name" : "croquette", + "members" : ["2cf91e02-a320-4766-aa9f-6efce3142d44"], + "leader":"2cf91e02-a320-4766-aa9f-6efce3142d44" + } + ], "challenges": [ { "id": "af0947e9-bf85-4233-8d50-2de787bf6021", diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index e46817f..2bce2e7 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -5,6 +5,7 @@ import Server = require('./Server'); import DashboardRouter = require('./api/DashboardRouter'); +import LoginRouter = require('./api/LoginRouter'); import BadgeRepository = require('./badge/BadgeRepository'); import BadgeFactory = require('./badge/BadgeFactory'); @@ -19,6 +20,9 @@ import UserRepository = require('./user/UserRepository'); import UserFactory = require('./user/UserFactory'); import User = require('./user/User'); +import TeamRepository = require('./user/TeamRepository'); +import TeamFactory = require('./user/TeamFactory'); + import Operand = require('./condition/expression/Operand'); import GoalExpression = require('./condition/expression/GoalExpression'); import OverallGoalCondition = require('./condition/OverallGoalCondition'); @@ -41,6 +45,9 @@ class Backend extends Server { public userRepository:UserRepository; public userFactory:UserFactory; + public teamRepository:TeamRepository; + public teamFactory:TeamFactory; + private storingHandler:StoringHandler; /** @@ -66,10 +73,13 @@ class Backend extends Server { this.userRepository = new UserRepository(); this.userFactory = new UserFactory(); + this.teamRepository = new TeamRepository(); + this.teamFactory = new TeamFactory(); + this.storingHandler = new StoringHandler(this); - this.buildAPI(); this.loadData(); + this.buildAPI(); } /** @@ -80,13 +90,14 @@ class Backend extends Server { buildAPI() { var self = this; - this.app.use('/dashboard', (new DashboardRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository,self.badgeRepository, new Middleware())).getRouter()); + this.app.use('/dashboard', (new DashboardRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository, self.teamRepository, self.badgeRepository, new Middleware())).getRouter()); + this.app.use('/login', (new LoginRouter(self.userRepository)).getRouter()); /* - this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); - this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.goalInstanceRepository, self.userRepository)).getRouter()); - this.app.use("/challenges", (new GoalInstanceRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); - */ + this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); + this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.goalInstanceRepository, self.userRepository)).getRouter()); + this.app.use("/challenges", (new GoalInstanceRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); + */ this.app.get('/test', function (req, res) { self.storingHandler.save( @@ -103,7 +114,7 @@ class Backend extends Server { loadData():void { var self = this; var result = self.storingHandler.load(); - if(result.success) { + if (result.success) { console.log(result.success); } else { diff --git a/backend/src/StoringHandler.ts b/backend/src/StoringHandler.ts index fcdd9df..3b4955a 100644 --- a/backend/src/StoringHandler.ts +++ b/backend/src/StoringHandler.ts @@ -50,6 +50,9 @@ class StoringHandler { this.fillUsersRepository(data); this.backend.userRepository.displayShortState(); + this.fillTeamRepository(data); + this.backend.teamRepository.displayShortState(); + console.log("___________________________________________________________"); return {success: '+++\tRepositories filled correctly\t+++'}; @@ -85,6 +88,17 @@ class StoringHandler { } } + fillTeamRepository(data) { + var teams = data.teams; + + for (var currentTeamIndex in teams) { + var currentTeamDescription = teams[currentTeamIndex]; + var currentTeam = this.backend.teamFactory.createTeam(currentTeamDescription, this.backend.userRepository); + this.backend.teamRepository.addTeam(currentTeam); + } + + } + fillChallengesRepository(data) { var challenges = data.challenges; diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index bdb4aa4..916b2c4 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -5,10 +5,14 @@ import ChallengeFactory = require('../challenge/ChallengeFactory'); import GoalRepository = require('../goal/GoalRepository'); import BadgeRepository = require('../badge/BadgeRepository'); import UserRepository = require('../user/UserRepository'); +import TeamRepository = require('../user/TeamRepository'); import Challenge = require('../challenge/Challenge'); import Clock = require('../Clock'); import ChallengeStatus = require('../Status'); import Goal = require('../goal/Goal'); +import User = require('../user/User'); +import Team = require('../user/Team'); +import Entity = require('../user/Entity'); import Middleware = require('../Middleware'); @@ -24,12 +28,17 @@ class DashboardRouter extends RouterItf { private userRepository:UserRepository; + private teamRepository:TeamRepository; + private badgeRepository:BadgeRepository; private middleware:Middleware; + // TODO DELETE THIS TOKEN WHEN SESSION WILL BE ESTABLISHED + private currentUser:Entity; - constructor(challengeRepository:ChallengeRepository, challengeFactory:ChallengeFactory, goalRepository:GoalRepository, userRepository:UserRepository, badgeRepository:BadgeRepository, middleware:Middleware) { + constructor(challengeRepository:ChallengeRepository, challengeFactory:ChallengeFactory, goalRepository:GoalRepository, + userRepository:UserRepository, teamRepository:TeamRepository, badgeRepository:BadgeRepository, middleware:Middleware) { super(); var fs = require('fs'); @@ -40,15 +49,75 @@ class DashboardRouter extends RouterItf { this.challengeFactory = challengeFactory; this.goalRepository = goalRepository; this.userRepository = userRepository; + this.teamRepository = teamRepository; this.badgeRepository = badgeRepository; this.middleware = middleware; + + this.currentUser = this.userRepository.getCurrentUser(); + console.log("CURRENT USER", this.currentUser); } buildRouter() { var self = this; + this.router.get('/view/:id/:dashboard?', function (req, res) { + + var userID = req.params.id; + var dashboardWanted = req.params.dashboard; + + console.log('UserID : ', userID, 'Dashboard wanted', dashboardWanted); + + var result:any = {}; + + var currentUser:User = self.userRepository.getUser(userID); + var team:Team = self.teamRepository.getTeam(dashboardWanted); + + if (currentUser == null && team == null) { + res.status(404).send('Euh ... Juste, tu n\'existes pas. Désolé. Bisous. Dégage'); + return; + } + + // User dashboard wanted + if (currentUser != null && team == null) { + result = self.getPersonalDashboard(currentUser); + } + + // Team dashboard wanted + else if (currentUser != null && team != null) { + //TODO Check if user is leader or member of team + + + var currentUserIsLeaderOfTargetedTeam = team.hasLeader(currentUser.getUUID()); + if (currentUserIsLeaderOfTargetedTeam) { + result = self.getTeamDashboardForALeader(team); + } + else { + result = self.getTeamDashboardForAMember(team); + } + } + // TODO extract method + // Build dashboardList, every views possible for current user + var dashboardList:any[] = []; + + var teams = self.teamRepository.getTeamsByMember(currentUser.getUUID()); + + + for (var teamIndex in teams) { + var currentTeam = teams[teamIndex]; + var teamDescription:any = {}; + teamDescription.id = currentTeam.getUUID(); + teamDescription.name = currentTeam.getName(); + dashboardList.push(teamDescription); + } + + result.dashboardList = dashboardList; + + res.send({data: result}); + }); + this.router.get('/', function (req, res) { console.log("Getting dashboard"); + // TODO redirect login page self.getDashboard(req, res); }); @@ -106,7 +175,10 @@ class DashboardRouter extends RouterItf { res.status(400).send({'error': 'goalID field is missing in request'}); } - var newChall:Challenge = this.createGoalInstance(goalID, Clock.getMoment(Clock.getNow())); + // TODO replace currentUser by user in the session + var currentUser = this.userRepository.getCurrentUser(); + + var newChall:Challenge = this.createGoalInstance(currentUser, goalID, Clock.getMoment(Clock.getNow())); if (newChall == null) { res.send({'error': 'Can not take this challenge'}); @@ -120,7 +192,7 @@ class DashboardRouter extends RouterItf { var goalID = req.params.id; try { - this.userRepository.getCurrentUser().deleteChallenge(goalID); + this.currentUser.deleteChallenge(goalID); res.send({"success": "Objectif supprimé !"}); } catch (e) { @@ -130,52 +202,50 @@ class DashboardRouter extends RouterItf { getDashboard(req, res) { console.log("\n=======================================================================\n---> Getting Dashboard\n"); - var result:any = {}; - try { + // TODO replace getCurrentUser by session user - // Third col : Evaluate challenge and return them - // Done before everything to be up to date - var challenges = this.userRepository.getCurrentUser().getChallenges(); - for (var challengeIndex in challenges) { - var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); - - this.evaluateChallenge(currentChallenge, currentChallengeID); - } + var result:any = {}; - // Build the description of updated challenges (potential additions/deletions) - var descriptionOfChallenges:any[] = []; - var challenges = this.userRepository.getCurrentUser().getChallenges(); - for (var challengeIndex in challenges) { - var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); - var currentChallengeDesc = currentChallenge.getDataInJSON(); - descriptionOfChallenges.push(currentChallengeDesc); + try { + // Build dashboardList, every views possible for current user + var dashboardList:any[] = []; + + var teams = this.teamRepository.getTeamsByMember(this.currentUser.getUUID()); + for (var teamIndex in teams) { + var team = teams[teamIndex]; + var teamDescription:any = {}; + teamDescription.id = team.getUUID(); + teamDescription.name = team.getName(); + dashboardList.push(teamDescription); } - // First col : available goal - var descriptionOfAvailableGoals = this.goalRepository.getListOfUntakedGoalInJSONFormat(this.userRepository.getCurrentUser(), this.challengeRepository); + console.log("Dashboard views available : ", dashboardList); - // Second col : badge description - var descriptionOfBadges:any[] = []; + var typeOfDashboardAsked:string = req.query.typeOfDashboard; - var badges = this.userRepository.getCurrentUser().getFinishedBadges(); - for (var currentBadgeIDIndex in badges) { - var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); - var dataTrophy = { - number: badges[currentBadgeIDIndex], - badge: currentBadge - }; + console.log("Dashboard view asked : ", typeOfDashboardAsked); - descriptionOfBadges.push(dataTrophy); + if (typeOfDashboardAsked == undefined || typeOfDashboardAsked === 'personal') { + result = this.getPersonalDashboard(this.currentUser); } + else { + var teamDescriptionWanted = this.teamRepository.getTeam(typeOfDashboardAsked); + console.log("Team dashboard mode, with team : \n\t", team.getStringDescription()); + // TODO DELETE THIS TOKEN WHEN SESSION WILL BE ESTABLISHED + // this.currentUser = teamDescriptionWanted; - // Build the response - result.goals = descriptionOfAvailableGoals; - result.badges = descriptionOfBadges; - result.challenges = descriptionOfChallenges; + // Check if current user is leader of the team + var currentUserIsLeaderOfTargetedTeam = teamDescriptionWanted.hasLeader(this.currentUser.getUUID()); + if (currentUserIsLeaderOfTargetedTeam) { + result = this.getTeamDashboardForALeader(teamDescriptionWanted); + } + else { + result = this.getTeamDashboardForAMember(teamDescriptionWanted); + } + } + result.dashboardList = dashboardList; res.send({success: 'Everything is fine', data: result}); console.log("\nSending ... \n", JSON.stringify(result)); @@ -185,10 +255,121 @@ class DashboardRouter extends RouterItf { catch (e) { res.send({error: e.toString()}); } + } + + getTeamDashboardForAMember(teamDescriptionWanted:Team):any { + var result:any = {}; + // Evaluate challenge and return them + // Done before everything to be up to date + this.evaluateChallengeForGivenEntity(teamDescriptionWanted); + // Second col : badge description + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(teamDescriptionWanted); + + // Third col : Build the description of updated challenges (potential additions/deletions) + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(teamDescriptionWanted); + + result.badges = descriptionOfBadges; + result.challenges = descriptionOfChallenges; + + return result; + } + + getTeamDashboardForALeader(teamDescriptionWanted:Team):any { + var result:any = {}; + + console.log("Current user is leader of given team"); + + // Evaluate challenge and return them + // Done before everything to be up to date + this.evaluateChallengeForGivenEntity(teamDescriptionWanted); + + // First col : available goal + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.challengeRepository); + + // Second col : badge description + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(teamDescriptionWanted); + + // Third col : Build the description of updated challenges (potential additions/deletions) + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(teamDescriptionWanted); + + // Build the response + result.goals = descriptionOfAvailableGoals; + result.badges = descriptionOfBadges; + result.challenges = descriptionOfChallenges; + + return result; + } + + getPersonalDashboard(user:User):any { + var result:any = {}; + + console.log("Personal Dashboard mode"); + + // Evaluate challenge and return them + // Done before everything to be up to date + this.evaluateChallengeForGivenEntity(this.currentUser); + + // First col : available goal + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(this.currentUser, this.challengeRepository); + + // Second col : badge description + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(this.currentUser); + + // Third col : Build the description of updated challenges (potential additions/deletions) + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(this.currentUser); + + // Build the response + result.goals = descriptionOfAvailableGoals; + result.badges = descriptionOfBadges; + result.challenges = descriptionOfChallenges; + + return result; + } + + private evaluateChallengeForGivenEntity(entity:Entity):void { + var challenges = entity.getChallenges(); + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + + this.evaluateChallenge(entity, currentChallenge, currentChallengeID); + } + } + + private buildCurrentChallengesDescriptionForGivenEntity(entity:Entity):any[] { + var descriptionOfChallenges:any[] = []; + + var challenges = entity.getChallenges() + ; + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + var currentChallengeDesc = currentChallenge.getDataInJSON(); + descriptionOfChallenges.push(currentChallengeDesc); + } + + return descriptionOfChallenges; + } + + private buildBadgesDescriptionForGivenEntity(entity:Entity):any[] { + var descriptionOfBadges:any[] = []; + + var badges = entity.getFinishedBadges(); + + for (var currentBadgeIDIndex in badges) { + var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); + var dataTrophy = { + number: badges[currentBadgeIDIndex], + badge: currentBadge + }; + + descriptionOfBadges.push(dataTrophy); + } + return descriptionOfBadges; } - private evaluateChallenge(challengeToEvaluate:Challenge, challengeID) { + private evaluateChallenge(entity, challengeToEvaluate:Challenge, challengeID) { var self = this; if (!DashboardRouter.DEMO) { @@ -211,10 +392,10 @@ class DashboardRouter extends RouterItf { function () { var result = challengeToEvaluate.evaluate(required); if (result) { - var newChall = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); - this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); + var newChall = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + this.addFinishedBadge(challengeID, entity.getUUID()); if (newChall != null) { - self.evaluateChallenge(newChall, newChall.getId()); + self.evaluateChallenge(entity, newChall, newChall.getId()); } } console.log("All data were retrieve properly"); @@ -241,12 +422,12 @@ class DashboardRouter extends RouterItf { console.log("Le challenge est réussi et terminé"); // Add finished badge to current user - this.addFinishedBadge(challengeID, this.userRepository.getCurrentUser().getUUID()); + this.addFinishedBadge(challengeID, entity.getUUID()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(newChallenge, newChallenge.getId()); + self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } } @@ -254,13 +435,12 @@ class DashboardRouter extends RouterItf { else if (!result && challengeToEvaluate.isFinished()) { console.log("Le challenge est FAIL et terminé"); - var user = this.userRepository.getCurrentUser(); - user.deleteChallenge(challengeToEvaluate.getId()); + entity.deleteChallenge(challengeToEvaluate.getId()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(newChallenge, newChallenge.getId()); + self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } } @@ -281,7 +461,7 @@ class DashboardRouter extends RouterItf { user.deleteChallenge(challengeID); } - createGoalInstance(goalID:string, date:moment.Moment):Challenge { + createGoalInstance(currentUser:User, goalID:string, date:moment.Moment):Challenge { // TODO ! stub ! // The data object below is a stub to manually // bind a symbolic name to a sensor name. @@ -307,7 +487,7 @@ class DashboardRouter extends RouterItf { } this.challengeRepository.addGoalInstance(goalInstance); - this.userRepository.getCurrentUser().addChallenge(goalInstance.getId()); + currentUser.addChallenge(goalInstance.getId()); return goalInstance; } } diff --git a/backend/src/api/LoginRouter.ts b/backend/src/api/LoginRouter.ts new file mode 100644 index 0000000..7f085a1 --- /dev/null +++ b/backend/src/api/LoginRouter.ts @@ -0,0 +1,41 @@ +import RouterItf = require('./RouterItf'); + +import UserRepository = require('../user/UserRepository'); +import User = require('../user/User'); + +class LoginRouter extends RouterItf { + private userRepository:UserRepository; + + constructor(userRepository:UserRepository) { + super(); + this.userRepository = userRepository; + } + + buildRouter() { + var self = this; + this.router.get('/', function (req, res) { + res.send({}); + }); + + this.router.post('/', function (req, res) { + console.log('Data received ', req.body); + + console.log('Login :', req.body.username); + + if(!req.body.username) { + res.status(400).send({error:'Field username is missing in request'}); + return; + } + + var currentUser:User = self.userRepository.getUserByName(req.body.username); + if(currentUser == null) { + res.status(400).send({error:'Given username can not be found'}); + return; + } + + res.send({success:'User profile was found', data:{token:currentUser.getUUID()}}); + }); + } +} + +export = LoginRouter; \ No newline at end of file diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index 6e873ee..c5e0cdd 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -59,7 +59,7 @@ class GoalDefinitionRepository { return null; } - getListOfUntakedGoalInJSONFormat(user:User, challengeRepository:ChallengeRepository) { + getListOfNotTakenGoalInJSONFormat(user:User, challengeRepository:ChallengeRepository) { var result = []; var currentChallengesID:string[] = user.getCurrentChallenges(); @@ -73,8 +73,6 @@ class GoalDefinitionRepository { var goals:Goal[] = this.diffBetweenTakenGoalsAndAvailableGoals(takenGoals, this.goals); - var result = []; - for (var goalIndex in goals) { var currentGoal = goals[goalIndex]; diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 848e243..2484094 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -1,5 +1,70 @@ -class Team { +import Entity = require('./Entity'); +import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); +import User = require('./User'); +class Team extends Entity { + private members:User[] = []; + private leader:User; + + constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}, members:User[] = [], leader:User = null) { + super(name, id, currentChallenges, finishedBadgesMap); + + this.members = members; + this.leader = leader; + } + + public hasLeader(aUserID:string):boolean { + return this.leader.hasUUID(aUserID); + } + + public getLeader() : User { + return this.leader; + } + + public addChallenge(challengeID:string):void { + + super.addChallenge(challengeID); + + for (var currentMemberIndex in this.members) { + var currentMember = this.members[currentMemberIndex]; + currentMember.addChallenge(challengeID); + } + } + + public deleteChallenge(challengeID:string):void { + super.deleteChallenge(challengeID); + + for (var currentMemberIndex in this.members) { + var currentMember = this.members[currentMemberIndex]; + currentMember.deleteChallenge(challengeID); + } + } + + public hasMember(aUserID:string):boolean { + for (var currentMemberIndex in this.members) { + var currentMember = this.members[currentMemberIndex]; + if (currentMember.hasUUID(aUserID)) { + return true; + } + } + + return false; + } + + public getStringDescription():string { + return 'Team:#' + this.getUUID() + '\t|Name : ' + this.getName() + '\t|LEADER : ' + this.leader + '\n'; + } + + public getStringDescriptionOfMembers() : string { + var result = ''; + + for (var currentMemberIndex in this.members) { + var currentMember = this.members[currentMemberIndex]; + result += '\t\t- ' + currentMember.getName() + '\n'; + } + + return result; + } } export = Team; \ No newline at end of file diff --git a/backend/src/user/TeamFactory.ts b/backend/src/user/TeamFactory.ts new file mode 100644 index 0000000..c469c77 --- /dev/null +++ b/backend/src/user/TeamFactory.ts @@ -0,0 +1,31 @@ +import Entity = require('./Entity'); +import Team = require('./Team'); +import User = require('./User'); +import UserRepository = require('./UserRepository'); + +class TeamFactory { + public createTeam(data:any, userRepository:UserRepository):Team { + var teamID:string = data.id; + var teamName:string = data.name; + + var currentChallenges:string[] = data.currentChallenges; + var finishedBadgesMap:any = data.finishedBadgesMap; + + var members:User[] = []; + + var membersIDs:string[] = data.members; + for (var membersIDsIndex in membersIDs) { + var currentMemberID = membersIDs[membersIDsIndex]; + var currentMember = userRepository.getUser(currentMemberID); + members.push(currentMember); + } + + var leaderID:string = data.leader; + var leader = userRepository.getUser(leaderID); + + var team:Team = new Team(teamName, teamID, currentChallenges, finishedBadgesMap, members, leader); + return team; + } +} + +export = TeamFactory; \ No newline at end of file diff --git a/backend/src/user/TeamRepository.ts b/backend/src/user/TeamRepository.ts new file mode 100644 index 0000000..1baa7e3 --- /dev/null +++ b/backend/src/user/TeamRepository.ts @@ -0,0 +1,74 @@ +import Team = require('./Team'); + +class TeamRepository { + + private teams:Team[] = [] + + teamExists(teamID:string, successCallBack:Function, failCallBack:Function) { + var team:Team = this.getTeam(teamID); + + (team != null) ? successCallBack(team) : failCallBack('User not found'); + } + + public addTeam(team:Team) { + this.teams.push(team); + } + + public getTeam(aUUID:string):Team { + for (var i in this.teams) { + var currentTeam = this.teams[i]; + if (currentTeam.hasUUID(aUUID)) { + return currentTeam; + } + } + + return null; + } + + getTeamsByMember(aUserID:string):Team[] { + var teams:Team[] = []; + + for(var currentTeamIndex in this.teams) { + var team = this.teams[currentTeamIndex]; + if(team.hasMember(aUserID)) { + teams.push(team); + } + } + + return teams; + } + + hasMember(aUserID:string) :boolean { + for(var currentTeamIndex in this.teams) { + var team = this.teams[currentTeamIndex]; + if(team.hasMember(aUserID)) { + return true; + } + } + + return false; + } + + public getDataInJSON():any { + var result:any[] = []; + + for (var currentTeamIndex in this.teams) { + var currentTeam = this.teams[currentTeamIndex]; + result.push(currentTeam.getDataInJSON()); + } + + return result; + } + + public displayShortState() { + console.log("\n\n+++\t Etat du repository des Teams\t+++"); + + for (var currentTeamIndex in this.teams) { + var currentTeam = this.teams[currentTeamIndex]; + console.log("#", currentTeam.getUUID(), "\t\nLeader:", currentTeam.getLeader().getName(), "\t| \tName : '", currentTeam.getName(), "'\n", "\tMembers:\n", currentTeam.getStringDescriptionOfMembers()); + + } + } +} + +export = TeamRepository; \ No newline at end of file diff --git a/backend/src/user/UserFactory.ts b/backend/src/user/UserFactory.ts index f60a27f..5cfa430 100644 --- a/backend/src/user/UserFactory.ts +++ b/backend/src/user/UserFactory.ts @@ -1,7 +1,8 @@ +import Entity = require('./Entity'); import User = require('./User'); class UserFactory { - public createUser(data:any):User { + public createUser(data:any):Entity { var userID:string = data.id; var userName:string = data.name; var currentChallenges:string[] = data.currentChallenges; diff --git a/backend/src/user/UserRepository.ts b/backend/src/user/UserRepository.ts index a0fd4e6..cbeb84d 100644 --- a/backend/src/user/UserRepository.ts +++ b/backend/src/user/UserRepository.ts @@ -16,6 +16,17 @@ class UserRepository { this.users.push(user); } + public getUserByName(username:string):User { + for (var i in this.users) { + var currentUser = this.users[i]; + if (currentUser.hasName(username)) { + return currentUser; + } + } + + return null; + } + public getUser(aUUID:string):User { for (var i in this.users) { var currentUser = this.users[i]; diff --git a/backend/tests/TeamTest.ts b/backend/tests/TeamTest.ts new file mode 100644 index 0000000..27c5a15 --- /dev/null +++ b/backend/tests/TeamTest.ts @@ -0,0 +1,60 @@ +/// +/// +/// + +import chai = require('chai'); +import sinon = require('sinon'); +var assert = chai.assert; + + +import Team = require('../src/user/Team'); +import User = require('../src/user/User'); + +describe("Teast a team", function () { + var aMember:User; + var anotherMember:User; + + var members:User[] = []; + var team:Team; + + beforeEach(() => { + aMember = new User('Gégé'); + anotherMember = new User('Dédé'); + members = [aMember, anotherMember]; + team = new Team("Croquette", null, [], [], members, aMember); + }); + + describe('Check its composition', () => { + + it('should have proper leader', () => { + chai.expect(team.hasLeader(aMember.getUUID())).to.be.true; + }); + + it('should have proper members', () => { + chai.expect(team.hasMember(aMember.getUUID())).to.be.true; + chai.expect(team.hasMember(anotherMember.getUUID())).to.be.true; + }); + }); + describe('Check add method', () => { + + it('should have a challenge when it was previously added', () => { + chai.expect(team.getChallenges()).to.be.eqls([]); + + var aChallengeID = 'aChallengeID'; + team.addChallenge(aChallengeID); + + chai.expect(team.getChallenges()).to.be.eqls([aChallengeID]); + }); + + it('should have added a challenge to its members when it was previously added', () => { + chai.expect(aMember.getChallenges()).to.be.eqls([]); + chai.expect(anotherMember.getChallenges()).to.be.eqls([]); + + var aChallengeID = 'aChallengeID'; + team.addChallenge(aChallengeID); + + chai.expect(aMember.getChallenges()).to.be.eqls([aChallengeID]); + chai.expect(anotherMember.getChallenges()).to.be.eqls([aChallengeID]); + }); + }); +}); diff --git a/backend/tests/integration/ChallengeBuildingTest.ts b/backend/tests/integration/ChallengeBuildingTest.ts index 4b7de6e..48733f9 100644 --- a/backend/tests/integration/ChallengeBuildingTest.ts +++ b/backend/tests/integration/ChallengeBuildingTest.ts @@ -21,6 +21,8 @@ import Goal = require('../../src/goal/Goal'); import RecurringSession = require('../../src/goal/RecurringSession'); import UserRepository = require('../../src/user/UserRepository'); +import TeamRepository = require('../../src/user/TeamRepository'); + import User = require('../../src/user/User'); import Clock = require('../../src/Clock'); @@ -28,26 +30,28 @@ import ChallengeStatus = require('../../src/Status'); import Middleware = require('../../src/Middleware'); - import DashboardRouter = require('../../src/api/DashboardRouter'); +import DashboardRouter = require('../../src/api/DashboardRouter'); - describe('Challenge integration test', () => { +describe('Challenge integration test', () => { // Important ! Allow us to set time - DashboardRouter.DEMO = true; + DashboardRouter.DEMO = true; var badgeRepository:BadgeRepository = new BadgeRepository(); var challengeRepository:ChallengeRepository = new ChallengeRepository(); var challengeFactory:ChallengeFactory = new ChallengeFactory(); var goalRepository:GoalRepository = new GoalRepository(badgeRepository); - // Build a default user / current user var userRepository:UserRepository = new UserRepository(); + var teamRepository:TeamRepository = new TeamRepository(); + + // Build a default user / current user var user:User = new User('Charlie'); userRepository.addUser(user); userRepository.setCurrentUser(user); // Init the router under test - var dashboardRouter:DashboardRouter = new DashboardRouter(challengeRepository, challengeFactory, goalRepository, userRepository, badgeRepository, new Middleware()); + var dashboardRouter:DashboardRouter = new DashboardRouter(challengeRepository, challengeFactory, goalRepository, userRepository, teamRepository, badgeRepository, new Middleware()); // Create a fake badge for fake goal var aBadgeName = 'Badge 1'; @@ -63,23 +67,23 @@ import Middleware = require('../../src/Middleware'); goalRepository.addGoal(aGoal); it('should have initialized the new challenge status to "RUN" when challenge is created during a working week', () => { - var newChallenge = dashboardRouter.createGoalInstance(aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); + var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.RUN); }); it('should have initialized the new challenge status to "WAITING" when challenge is created during week-end', () => { // The goal is recurrent every week (monday-friday). A goal created saturday must be in WAITING status - var newChallenge = dashboardRouter.createGoalInstance(aGoal.getUUID(), moment("2015-08-08T12:15:00+02:00")); + var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-08T12:15:00+02:00")); chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.WAIT); }); it('should have set the startDate to monday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createGoalInstance(aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); + var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(startDate.toISOString()); }); it('should have set the endDate to friday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createGoalInstance(aGoal.getUUID(), moment("2015-08-07T23:59:59+02:00")); + var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-07T23:59:59+02:00")); chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(startDate.toISOString()); }); }); \ No newline at end of file diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index e12149e..e55bf4e 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -19,6 +19,33 @@ var app = angular 'datePicker' ]) .config(function ($routeProvider) { + + $routeProvider + .when('/', { + templateUrl: '../views/login.html', + controller: 'LoginCtrl', + controllerAs: 'login' + }) + .when('/dashboard/view/', { + templateUrl: '../views/dashboard.html', + controller: 'DashboardCtrl', + controllerAs:'dashboard' + }) + .when('/dashboard/view/:id/', { + templateUrl: '../views/dashboard.html', + controller: 'DashboardCtrl', + controllerAs:'dashboard' + }) + .when('/dashboard/view/:id/:dashboardType', { + templateUrl: '../views/dashboard.html', + controller: 'DashboardCtrl', + controllerAs:'dashboard' + }) + .otherwise({ + redirectTo: '/lolerreurdansredirectionangulareuuuh/' + }); + + /* $routeProvider .when('/', { templateUrl: '../views/homepage/homepage.html', @@ -60,6 +87,7 @@ var app = angular .otherwise({ redirectTo: '/' }); + */ }); diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js index 487da29..e075c96 100644 --- a/frontend/app/scripts/controllers/ServiceDashboard.js +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -4,18 +4,25 @@ var dashboardBasePath = 'http://localhost:3000/dashboard/'; var app = angular.module('ecoknowledgeApp'); -app.service('ServiceDashboard', ['$http', function ServiceDashboard($http) { +app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function ServiceDashboard($http, $rootScope, $cookies) { - this.get = function (successFunc, failFunc) { - console.log('Service dashboard : Get On ', dashboardBasePath); + this.get = function (successFunc, failFunc, dashboardWanted) { - $http.get(dashboardBasePath) + console.log('TOKEN???', $cookies.get('token')); + + var path = dashboardBasePath + '/view/' +$cookies.get('token') + '/' + $cookies.get('dashboardWanted'); + + console.log('Service dashboard : Get On ', path); + + $http.get(path) .success(function (data) { var goals = data.data.goals; var badges = data.data.badges; var challenges = data.data.challenges; - successFunc(data, goals, badges, challenges); + var dashboardViews = data.data.dashboardList; + + successFunc(data, goals, badges, challenges, dashboardViews); }) .error(function (data) { console.error('ServiceDashboard : fail get dashboard', data); diff --git a/frontend/app/scripts/controllers/ServiceLogin.js b/frontend/app/scripts/controllers/ServiceLogin.js index e834f47..1079ee3 100644 --- a/frontend/app/scripts/controllers/ServiceLogin.js +++ b/frontend/app/scripts/controllers/ServiceLogin.js @@ -6,12 +6,24 @@ var loginBasePath = 'http://localhost:3000/login/'; app.service('ServiceLogin', ['$http', function ServiceLogin($http) { - this.login = function (name, successFunc, failFunc) { - var path = loginBasePath + ''; - console.log('ServiceLogin : login', path); + this.get = function(successFunc, failFunc) { + $http.get(loginBasePath) + .success(function (data) { + successFunc(data); + }) + .error(function (data) { + failFunc(data); + }); + }; - $http.post(path, name) + this.login = function (username, successFunc, failFunc) { + + var path = loginBasePath + ''; + var dataToSend = {username:username}; + console.log('ServiceLogin : login', path, 'with data', dataToSend); + $http.post(path, dataToSend) .success(function (data) { + console.log('Current user has obtained token : ', data.data.token); successFunc(data); }) .error(function (data) { diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index d6e8b79..6b29aae 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -2,13 +2,16 @@ var app = angular.module('ecoknowledgeApp'); -app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', function (ServiceDashboard, $window, $location) { +app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$cookies', function (ServiceDashboard, $window, $location, $cookies) { var self = this; self.goals = {}; self.trophies = {}; self.challenges = {}; + self.dashboardViews = []; + self.dashboardWanted = ''; + // Debug self.request = {}; @@ -16,7 +19,7 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', fun console.log('Angular wanna get the dashboard'); ServiceDashboard.get( - function (data, goals, badges, challenges) { + function (data, goals, badges, challenges, dashboardViews) { console.log('Result of dashboard : ', goals, badges, challenges); self.request = data; @@ -24,13 +27,36 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', fun self.goals = goals; self.trophies = badges; self.challenges = challenges; + self.dashboardViews = dashboardViews; }, function (data) { - console.error("Redirection vers", data.redirectTo); - $location.path(data.redirectTo); + console.error('Redirection vers', data.redirectTo); + //$location.path(data.redirectTo); }); }; + this.changeDashboardView = function() { + console.log('Angular wanna change the dashboard'); + + $cookies.put('dashboardWanted', self.dashboardWanted); + + ServiceDashboard.get( + function (data, goals, badges, challenges, dashboardViews) { + console.log('Result of dashboard : ', goals, badges, challenges); + + self.request = data; + + self.goals = goals; + self.trophies = badges; + self.challenges = challenges; + self.dashboardViews = dashboardViews; + }, + function (data) { + console.error('Redirection vers', data.redirectTo); + $location.path(data.redirectTo); + }, self.dashboardWanted ); + }; + self.takeGoal = function (goalID) { var toSend = {}; toSend.id = goalID; diff --git a/frontend/app/scripts/controllers/login.js b/frontend/app/scripts/controllers/login.js index f53c956..3c4c4d9 100644 --- a/frontend/app/scripts/controllers/login.js +++ b/frontend/app/scripts/controllers/login.js @@ -2,23 +2,23 @@ var app = angular.module('ecoknowledgeApp'); -app.controller('LoginCtrl', ['ServiceLogin', function (ServiceLogin) { - - var self = this; - - this.username = '' - this.password = ''; +app.controller('LoginCtrl', ['ServiceLogin', '$rootScope', '$location', '$cookies', function (ServiceLogin, $rootScope, $location, $cookies) { + this.username = ''; this.debug = ''; + var self = this; this.connect = function () { - self.debug = 'ID:' + self.username + ', passwd:' + self.password; ServiceLogin.login(self.username, function (data) { console.log('Login success: data received', data); + $cookies.put('token', data.data.token); + console.log('Token stored : ', $cookies.get('token')); + var pathToDashboard = '/dashboard/view/' + data.data.token; + console.log('Redirection vers', pathToDashboard); + $location.path(pathToDashboard); }, function (data) { console.log('Login fail: data received', data); - }); }; diff --git a/frontend/app/views/dashboard.html b/frontend/app/views/dashboard.html index 3b3b28e..51ac8bc 100644 --- a/frontend/app/views/dashboard.html +++ b/frontend/app/views/dashboard.html @@ -1,7 +1,25 @@ -

EcoKnowledge

- - - +
+ +
+{{dashboard.dashboardViews}} + + + +
+ +
+ + + + +
diff --git a/frontend/app/views/homepage/homepage.html b/frontend/app/views/homepage/homepage.html index 3b3b28e..9755853 100644 --- a/frontend/app/views/homepage/homepage.html +++ b/frontend/app/views/homepage/homepage.html @@ -1,6 +1,8 @@

EcoKnowledge

+ + diff --git a/frontend/app/views/login.html b/frontend/app/views/login.html index b8750b1..2443e99 100644 --- a/frontend/app/views/login.html +++ b/frontend/app/views/login.html @@ -5,20 +5,13 @@ - {{loginCtrl.username}} - {{loginCtrl.password}} - {{loginCtrl.debug}} -
+ {{login}} +
-
+
- -
-
-
-
-
+
From cd041a72c0df5c2bd74210252681bcce7dd456cd Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Fri, 28 Aug 2015 09:56:47 +0200 Subject: [PATCH 09/28] Start to dev TeamChallenges --- backend/Gruntfile.js | 3 +- backend/db.json | 54 ++------ backend/src/Backend.ts | 20 ++- backend/src/StoringHandler.ts | 20 ++- backend/src/api/DashboardRouter.ts | 57 ++++----- backend/src/challenge/Challenge.ts | 67 +++++----- backend/src/challenge/ChallengeFactory.ts | 147 +++++++--------------- backend/src/challenge/TeamChallenge.ts | 3 + backend/src/goal/Goal.ts | 3 +- backend/src/goal/GoalRepository.ts | 4 +- backend/src/user/Entity.ts | 4 + backend/src/user/Team.ts | 4 +- backend/src/user/User.ts | 140 ++++++++++++++++++++- backend/src/user/UserFactory.ts | 7 +- backend/src/user/UserRepository.ts | 2 +- backend/tests/database/UserStoreTest.ts | 2 +- 16 files changed, 289 insertions(+), 248 deletions(-) create mode 100644 backend/src/challenge/TeamChallenge.ts diff --git a/backend/Gruntfile.js b/backend/Gruntfile.js index 55674cd..b15ef83 100644 --- a/backend/Gruntfile.js +++ b/backend/Gruntfile.js @@ -119,7 +119,8 @@ module.exports = function (grunt) { grunt.registerTask('build', function () { grunt.task.run(['clean:build','clean:test']); - grunt.task.run(['typescript:build', 'typescript:test']); + // grunt.task.run(['typescript:build', 'typescript:test']); + grunt.task.run(['typescript:build']); }); grunt.registerTask('develop', function() { diff --git a/backend/db.json b/backend/db.json index dd91a65..9d464e0 100644 --- a/backend/db.json +++ b/backend/db.json @@ -109,7 +109,10 @@ "finishedBadgesMap": { "44bb8108-8830-4f43-abd1-3ef643303d92": 1, "fde68334-f515-4563-954b-ac91b4a42f88": 1 - } + }, + "mapSymbolicNameToSensor": { + "TMP_CLI":"TEMP_443V" + } } ], "teams": [ @@ -127,44 +130,8 @@ "timeProgress": 100, "startDate": "2015-08-02T22:00:00.000Z", "endDate": "2015-08-07T21:59:59.999Z", - "goal": { - "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "conditions": { - "TMP_CLI": "TEMP_443V" - } - }, - "progress": [ - { - "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", - "expression": { - "valueLeft": { - "value": "TMP_CLI", - "sensor": true - }, - "valueRight": { - "value": "15", - "sensor": false - }, - "comparison": ">", - "description": "a desc", - "periodOfTime": "-2208474000000" - }, - "threshold": 25, - "startDate": "2015-07-26T22:00:00.000Z", - "dateOfCreation": "2015-08-02T22:00:00.000Z", - "endDate": "2015-08-07T21:59:59.999Z", - "percentageAchieved": 100, - "percentageOfTimeElapsed": 100, - "filter": { - "dayOfWeekFilter": "all", - "periodOfDayFilter": [ - "morning", - "afternoon" - ] - }, - "type": "comparison" - } - ], + "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", "status": "SUCCESS" }, { @@ -173,13 +140,8 @@ "timeProgress": 0, "startDate": "2015-08-09T22:00:00.000Z", "endDate": "2015-08-14T21:59:59.999Z", - "goal": { - "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "conditions": { - "TMP_CLI": "TEMP_443V" - } - }, - "progress": [], + "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", "status": "WAIT" } ] diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index 2bce2e7..96dcdbf 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -39,8 +39,8 @@ class Backend extends Server { public goalDefinitionRepository:GoalRepository; public goalDefinitionFactory:GoalFactory; - public goalInstanceRepository:ChallengeRepository; - public goalInstanceFactory:ChallengeFactory; + public challengeRepository:ChallengeRepository; + public challengeFactory:ChallengeFactory; public userRepository:UserRepository; public userFactory:UserFactory; @@ -67,8 +67,8 @@ class Backend extends Server { this.goalDefinitionRepository = new GoalRepository(this.badgeRepository); this.goalDefinitionFactory = new GoalFactory(); - this.goalInstanceRepository = new ChallengeRepository(); - this.goalInstanceFactory = new ChallengeFactory(); + this.challengeRepository = new ChallengeRepository(); + this.challengeFactory = new ChallengeFactory(); this.userRepository = new UserRepository(); this.userFactory = new UserFactory(); @@ -90,13 +90,13 @@ class Backend extends Server { buildAPI() { var self = this; - this.app.use('/dashboard', (new DashboardRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository, self.teamRepository, self.badgeRepository, new Middleware())).getRouter()); + this.app.use('/dashboard', (new DashboardRouter(self.challengeRepository, self.challengeFactory, self.goalDefinitionRepository, self.userRepository, self.teamRepository, self.badgeRepository, new Middleware())).getRouter()); this.app.use('/login', (new LoginRouter(self.userRepository)).getRouter()); /* this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); - this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.goalInstanceRepository, self.userRepository)).getRouter()); - this.app.use("/challenges", (new GoalInstanceRouter(self.goalInstanceRepository, self.goalInstanceFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); + this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.challengeRepository, self.userRepository)).getRouter()); + this.app.use("/challenges", (new GoalInstanceRouter(self.challengeRepository, self.challengeFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); */ this.app.get('/test', function (req, res) { @@ -112,14 +112,10 @@ class Backend extends Server { } loadData():void { - var self = this; - var result = self.storingHandler.load(); + var result = this.storingHandler.load(); if (result.success) { console.log(result.success); } - else { - this.userRepository.setCurrentUser(new User('Jackie')); - } } } diff --git a/backend/src/StoringHandler.ts b/backend/src/StoringHandler.ts index 3b4955a..084f2b1 100644 --- a/backend/src/StoringHandler.ts +++ b/backend/src/StoringHandler.ts @@ -30,7 +30,7 @@ class StoringHandler { result['definitions'] = this.backend.goalDefinitionRepository.getDataInJSON(); result['badges'] = this.backend.badgeRepository.getDataInJSON(); result['users'] = this.backend.userRepository.getDataInJSON(); - result['challenges'] = this.backend.goalInstanceRepository.getDataInJSON(); + result['challenges'] = this.backend.challengeRepository.getDataInJSON(); this.serializer.save(result, successCallBack, failCallBack); } @@ -44,15 +44,15 @@ class StoringHandler { this.fillBadgesRepository(data); this.backend.badgeRepository.displayShortState(); - this.fillChallengesRepository(data); - this.backend.goalInstanceRepository.displayShortState(); - this.fillUsersRepository(data); this.backend.userRepository.displayShortState(); this.fillTeamRepository(data); this.backend.teamRepository.displayShortState(); + this.fillChallengesRepository(data); + this.backend.challengeRepository.displayShortState(); + console.log("___________________________________________________________"); return {success: '+++\tRepositories filled correctly\t+++'}; @@ -82,7 +82,7 @@ class StoringHandler { for (var currentUserIndex in users) { var currentUserDescription = users[currentUserIndex]; - var currentUser = this.backend.userFactory.createUser(currentUserDescription); + var currentUser = this.backend.userFactory.createUser(currentUserDescription, this.backend.challengeFactory); this.backend.userRepository.addUser(currentUser); this.backend.userRepository.setCurrentUser(currentUser); } @@ -102,16 +102,12 @@ class StoringHandler { fillChallengesRepository(data) { var challenges = data.challenges; - for (var currentChallengeIndex = 0 ; currentChallengeIndex < challenges.length ; currentChallengeIndex++) { + for (var currentChallengeIndex = 0; currentChallengeIndex < challenges.length; currentChallengeIndex++) { var currentChallengeDescription = challenges[currentChallengeIndex]; - var currentChallenge = this.backend.goalInstanceFactory.createGoalInstance(currentChallengeDescription, - this.backend.goalDefinitionRepository, - this.backend.userRepository, - - Clock.getMoment(Clock.getNow())); + var currentChallenge = this.backend.challengeFactory.restoreChallenge(currentChallengeDescription, this.backend.goalDefinitionRepository, this.backend.userRepository, Clock.getMoment(Clock.getNow())); - this.backend.goalInstanceRepository.addGoalInstance(currentChallenge); + this.backend.challengeRepository.addGoalInstance(currentChallenge); } } } diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 916b2c4..c02c43e 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -52,9 +52,6 @@ class DashboardRouter extends RouterItf { this.teamRepository = teamRepository; this.badgeRepository = badgeRepository; this.middleware = middleware; - - this.currentUser = this.userRepository.getCurrentUser(); - console.log("CURRENT USER", this.currentUser); } buildRouter() { @@ -227,7 +224,7 @@ class DashboardRouter extends RouterItf { console.log("Dashboard view asked : ", typeOfDashboardAsked); if (typeOfDashboardAsked == undefined || typeOfDashboardAsked === 'personal') { - result = this.getPersonalDashboard(this.currentUser); + result = this.getPersonalDashboard(null); } else { var teamDescriptionWanted = this.teamRepository.getTeam(typeOfDashboardAsked); @@ -261,7 +258,7 @@ class DashboardRouter extends RouterItf { var result:any = {}; // Evaluate challenge and return them // Done before everything to be up to date - this.evaluateChallengeForGivenEntity(teamDescriptionWanted); + this.evaluateChallengeForGivenTeam(teamDescriptionWanted); // Second col : badge description var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(teamDescriptionWanted); @@ -281,7 +278,7 @@ class DashboardRouter extends RouterItf { // Evaluate challenge and return them // Done before everything to be up to date - this.evaluateChallengeForGivenEntity(teamDescriptionWanted); + this.evaluateChallengeForGivenTeam(teamDescriptionWanted); // First col : available goal var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.challengeRepository); @@ -307,10 +304,10 @@ class DashboardRouter extends RouterItf { // Evaluate challenge and return them // Done before everything to be up to date - this.evaluateChallengeForGivenEntity(this.currentUser); + this.evaluateChallengeForGivenUser(user); // First col : available goal - var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(this.currentUser, this.challengeRepository); + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(user, this.challengeRepository); // Second col : badge description var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(this.currentUser); @@ -326,13 +323,23 @@ class DashboardRouter extends RouterItf { return result; } - private evaluateChallengeForGivenEntity(entity:Entity):void { - var challenges = entity.getChallenges(); + + private evaluateChallengeForGivenTeam(team:Team):void { + var challenges = team.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); - this.evaluateChallenge(entity, currentChallenge, currentChallengeID); + this.evaluateChallenge(team, currentChallenge, currentChallengeID); + } + } + private evaluateChallengeForGivenUser(user:User):void { + var challenges = user.getCurrentChallenges(); + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + + this.evaluateChallenge(user, currentChallenge, currentChallengeID); } } @@ -393,7 +400,7 @@ class DashboardRouter extends RouterItf { var result = challengeToEvaluate.evaluate(required); if (result) { var newChall = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); - this.addFinishedBadge(challengeID, entity.getUUID()); + this.addBadge(challengeID, entity.getUUID()); if (newChall != null) { self.evaluateChallenge(entity, newChall, newChall.getId()); } @@ -457,38 +464,22 @@ class DashboardRouter extends RouterItf { */ var user = this.userRepository.getUser(userID); var badgeID = this.challengeRepository.getBadgeByChallengeID(challengeID); - user.addFinishedBadge(badgeID); + user.addBadge(badgeID); user.deleteChallenge(challengeID); } createGoalInstance(currentUser:User, goalID:string, date:moment.Moment):Challenge { - // TODO ! stub ! - // The data object below is a stub to manually - // bind a symbolic name to a sensor name. - // In the future, this won't be hardcoded but - // will be set by final user during the account - // creation process - - var data = - { - "goal": { - "id": goalID, - "conditions": {"TMP_CLI": "TEMP_443V"} - } - }; var goal:Goal = this.goalRepository.getGoal(goalID); - //console.log("Je construit un challenge en partant du principe que nous sommes le ", date.toISOString()); - var goalInstance = this.challengeFactory.createGoalInstance(data, this.goalRepository, null, date); + var newChallenge = currentUser.addChallenge(goal, date); - if (goalInstance.getEndDate().isAfter(goal.getEndDate())) { + if (newChallenge == null) { return null; } - this.challengeRepository.addGoalInstance(goalInstance); - currentUser.addChallenge(goalInstance.getId()); - return goalInstance; + this.challengeRepository.addGoalInstance(newChallenge); + return newChallenge; } } diff --git a/backend/src/challenge/Challenge.ts b/backend/src/challenge/Challenge.ts index 750fedd..006740e 100644 --- a/backend/src/challenge/Challenge.ts +++ b/backend/src/challenge/Challenge.ts @@ -16,7 +16,7 @@ import Clock = require('../Clock'); class Challenge { private id:string; - private goalDefinition:Goal; + private goal:Goal; private startDate:moment.Moment; private endDate:moment.Moment; @@ -25,16 +25,15 @@ class Challenge { private status:BadgeStatus; private progress:any[] = []; - private percentageOfTime:number = 0; // { 'tmp_cli':'ac_443', 'tmp_ext':'TEMP_444', 'door_o':'D_55', ... } private mapSymbolicNameToSensor:any = {}; - constructor(startDate:moment.Moment, endDate:moment.Moment, description:string, goal:Goal, - mapGoalToConditionAndSensor:any, id = null) { + private user:User; - //onsole.log("Building the challenge with startdate", startDate.format(), "and enddate", endDate.format()); + constructor(goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, description:string='', + mapGoalToConditionAndSensor:any={}, id = null) { this.id = (id) ? id : UUID.v4(); this.description = description; @@ -42,23 +41,28 @@ class Challenge { this.startDate = startDate; this.endDate = endDate; - this.goalDefinition = goal; - this.goalDefinition.setTimeBoxes(new TimeBox(startDate, endDate)); + this.goal = goal; + // TODO DELETE THIS TOKEN + this.goal.setTimeBoxes(new TimeBox(startDate, endDate)); this.mapSymbolicNameToSensor = mapGoalToConditionAndSensor; + this.user = user; this.status = BadgeStatus.RUN; } public updateDurationAchieved(currentDate:number) { - var duration:number = this.endDate.valueOf() - this.startDate.valueOf(); + var current:moment.Moment = Clock.getMoment(currentDate); + if (current.isBefore(this.startDate)) { throw new Error('Time given is before dateOfCreation !'); } - var durationAchieved:number = current.valueOf() - this.startDate.valueOf(); + var duration:number = this.endDate.valueOf() - this.startDate.valueOf(); + var durationAchieved:number = current.valueOf() - this.startDate.valueOf(); this.percentageOfTime = durationAchieved * 100 / duration; + // It can have tiny incorrect decimal values this.percentageOfTime = (this.percentageOfTime > 100) ? 100 : this.percentageOfTime; } @@ -92,15 +96,15 @@ class Challenge { } public getGoalDefinition():Goal { - return this.goalDefinition; + return this.goal; } public getBadge():string { - return this.goalDefinition.getBadgeID(); + return this.goal.getBadgeID(); } public getName():string { - return this.goalDefinition.getName(); + return this.goal.getName(); } public getId():string { @@ -134,14 +138,14 @@ class Challenge { for (var currentSymbolicName in this.mapSymbolicNameToSensor) { var currentSensor = this.mapSymbolicNameToSensor[currentSymbolicName]; - result[currentSensor] = this.goalDefinition.getRequired()[currentSymbolicName]; + result[currentSensor] = this.goal.getRequired()[currentSymbolicName]; } return result; } setGoal(goal) { - this.goalDefinition = goal; + this.goal = goal; } public haveToStart(now:moment.Moment):boolean { @@ -163,7 +167,7 @@ class Challenge { public evaluate(values:any):boolean { console.log('evaluate de challenge'); // Check if badge is running. If Waiting or failed, it must be left unchanged - if(this.status != BadgeStatus.RUN) { + if (this.status != BadgeStatus.RUN) { return false; } @@ -174,14 +178,14 @@ class Challenge { var numberOfValuesNeeded = Object.keys(this.mapSymbolicNameToSensor).length; if (numberOfValues < numberOfValuesNeeded) { - throw new Error("Can not evaluate goal " + this.goalDefinition.getName() + throw new Error("Can not evaluate goal " + this.goal.getName() + "! There are " + numberOfValuesNeeded + " symbolic names needed and only " + numberOfValues + " values given"); } var mapSymbolicNameToValue = this.bindSymbolicNameToValue(values); - var resultEval = this.goalDefinition.evaluate(mapSymbolicNameToValue, this); + var resultEval = this.goal.evaluate(mapSymbolicNameToValue, this); if (resultEval && this.percentageOfTime >= 100) { this.status = BadgeStatus.SUCCESS; @@ -218,23 +222,30 @@ class Challenge { timeProgress: this.percentageOfTime, startDate: this.startDate, endDate: this.endDate, - goal: { - id: this.goalDefinition.getUUID(), - conditions: this.mapSymbolicNameToSensor - }, - progress: this.progress, + goal: this.goal.getUUID(), + user: this.user.getUUID(), status: this.getStatusAsString() } } private getStatusAsString():string { - switch(this.status){ - case 0: return 'WAIT'; break; - case 1: return 'RUNNING'; break; - case 2: return 'SUCCESS'; break; - case 3: return 'FAIL'; break; - default: return 'UNKNOWN'; break; + switch (this.status) { + case 0: + return 'WAIT'; + break; + case 1: + return 'RUNNING'; + break; + case 2: + return 'SUCCESS'; + break; + case 3: + return 'FAIL'; + break; + default: + return 'UNKNOWN'; + break; } } } diff --git a/backend/src/challenge/ChallengeFactory.ts b/backend/src/challenge/ChallengeFactory.ts index 78d4924..6596ed3 100644 --- a/backend/src/challenge/ChallengeFactory.ts +++ b/backend/src/challenge/ChallengeFactory.ts @@ -3,6 +3,7 @@ /// import Challenge = require('./Challenge'); +import User = require('../user/User'); import Goal = require('../goal/Goal'); import GoalRepository = require('../goal/GoalRepository'); import UserRepository = require('../user/UserRepository'); @@ -11,139 +12,79 @@ import TimeBox = require('../TimeBox'); import ChallengeStatus = require('../Status'); import Clock = require('../Clock'); +import BadArgumentException = require('../exceptions/BadArgumentException'); + var moment = require('moment'); var moment_timezone = require('moment-timezone'); class GoalInstanceFactory { - /** - * - * @param data - * { - * description : , - * timeBox - * goal : - * { - * id : , - * conditions : - * { - * : - * } - * } - * } - * @param goalRepository - * @param userProvider - * @returns {Challenge} - */ - public createGoalInstance(data:any, goalRepository:GoalRepository, userProvider:UserRepository, now:moment.Moment):Challenge { - - var challengeID = data.id; - - var challengeDescription:string = data.description; - - var goalJSONDesc:any = data.goal; - var goalID = goalJSONDesc.id; - var goal:Goal = goalRepository.getGoal(goalID); + restoreChallenge(data:any, goalRepository:GoalRepository, userRepository:UserRepository, now:moment.Moment):Challenge { - if (goal == null) { - throw new Error('Can not create goal instance because ID given of goal definition can not be found'); + var goalID:string = data.goalID; + if(goalID == null) { + throw new BadArgumentException('Can not restore given challenge because goalID is null'); } - // Check if challenge is built from db (startDate and endDate are provided in data parameter) - // Or if challengeFactory was called from a 'newChallenge' method + var userID:string = data.userID; + if(userID == null) { + throw new BadArgumentException('Can not restore given challenge because userID is null'); + } - var nowDate:moment.Moment = now; - var mapGoalsToConditionAndSensors:any = goalJSONDesc.conditions; + var goal:Goal = goalRepository.getGoal(goalID); + if(goal == null) { + throw new BadArgumentException('Can not restore given challenge because goal with id ' + goalID + ' was not found'); + } - // We restore it from DB - if (data.startDate != null) { - return this.restoreChallenge(challengeID, data, goal, challengeDescription, goalRepository, mapGoalsToConditionAndSensors, now); + var user:User = userRepository.getUser(userID); + if(user == null) { + throw new BadArgumentException('Can not restore given challenge because user with id ' + userID + ' was not found'); } - // The user just took a new challenge + var startDateDesc = data.startDate; + var endDateDesc = data.endDate; - var clone = now.clone(); - //console.log("NOW?", clone.format()); + var startDate = Clock.getMoment(startDateDesc); + var endDate = Clock.getMoment(endDateDesc); - var startDate = goal.getStartDateOfSession(clone); - var endDate = goal.getEndDateOfSession(clone.clone()); + var challenge:Challenge = this.createChallenge(goal, user, now, startDate, endDate); /* - console.log("START DATE OF SESSION", startDate.format()); - console.log("START DATE OF SESSION", startDate.toISOString()); + var challenge:Challenge = new Challenge(startDate, endDate, goalInstanceDescription, goalDefinition, mapGoalsToConditionAndSensors, id); - console.log("END DATE OF SESSION", endDate.format()); - console.log("END DATE OF SESSION", endDate.toISOString()); - */ - - /* - if(!this.checkDates(goalDefinition,startDate)) { - throw new Error('Can not build goal instance ! it does not have the time to be achieved. We are the ' - + now + ', the goal start the' + goalDefinition.getStartDate() + ' and end the ' +goalDefinition.getEndDate() + ' with a duration of ' + goalDefinition.getDuration() + ' days'); + if (now.isBefore(startDate)) { + //console.log("Le challenge est en WAIT"); + challenge.setStatus(ChallengeStatus.WAIT); + } + if (now.isAfter(startDate) && now.isBefore(endDate)) { + ///console.log("Le challenge est en RUN"); + challenge.setStatus(ChallengeStatus.RUN); } */ - - var challenge:Challenge = new Challenge(startDate, endDate, challengeDescription, goal, mapGoalsToConditionAndSensors, challengeID); - - - if(now.isBefore(startDate)) { - //console.log("Le challenge est en WAIT"); - challenge.setStatus(ChallengeStatus.WAIT); - } - if(now.isAfter(startDate) && now.isBefore(endDate)) { - ///console.log("Le challenge est en RUN"); - challenge.setStatus(ChallengeStatus.RUN); - } - - // TODO attach badge to user - // user.addBadgeByDescription(badge); - - // console.log("L'objectif ", goalDefinition.getName(), "a ete instancie ! Intervalle de vie de l'objectif : du", startDate, "au", endDate); - return challenge; } - private restoreChallenge(id, data:any, goalDefinition:Goal, goalInstanceDescription:string, goalRepository:GoalRepository, mapGoalsToConditionAndSensors, now:moment.Moment):Challenge{ - //console.log("RESTORE CHALLENGE"); - - var startDateDesc = data.startDate; - var endDateDesc = data.endDate; + createChallenge(goal:Goal, user:User, now:moment.Moment, startDateSaved = null, endDateSaved = null):Challenge { + var clone = now.clone(); - var startDate = Clock.getMoment(startDateDesc); - var endDate = Clock.getMoment(endDateDesc); + var startDateOfChallenge = (startDateSaved == null) ? goal.getStartDateOfSession(clone) : startDateSaved; + var endDateOfChallenge = (endDateSaved == null) ? goal.getEndDateOfSession(clone.clone()) : endDateSaved; - var challenge:Challenge = new Challenge(startDate, endDate, goalInstanceDescription, goalDefinition, mapGoalsToConditionAndSensors, id); + var newChallenge:Challenge = new Challenge(goal, user, startDateOfChallenge, endDateOfChallenge); - if(now.isBefore(startDate)) { - //console.log("Le challenge est en WAIT"); - challenge.setStatus(ChallengeStatus.WAIT); + if (newChallenge.getEndDate().isAfter(goal.getEndDate())) { + return null; } - if(now.isAfter(startDate) && now.isBefore(endDate)) { - ///console.log("Le challenge est en RUN"); - challenge.setStatus(ChallengeStatus.RUN); + if (now.isBefore(startDateOfChallenge)) { + newChallenge.setStatus(ChallengeStatus.WAIT); + } + if (now.isAfter(startDateOfChallenge) && now.isBefore(endDateOfChallenge)) { + newChallenge.setStatus(ChallengeStatus.RUN); } - return challenge; - } - - private realignStartAndEndDates(goal:Goal, now:moment.Moment) { - - } - - /** - * Check if the goal instance is started today, Date.now + goalDefinition#duration <= goalDefinition#endDate - * @param goalDefinition - */ - public checkDates(goalDefinition:Goal, startDate:moment.Moment):boolean { - var durationInDays:number = goalDefinition.getDuration(); - - var endDate:moment.Moment = Clock.getMoment(0).year(startDate.year()).month(startDate.month()).date(startDate.date() + durationInDays); - var endDateOfValidityPeriod:moment.Moment = goalDefinition.getEndDate(); - - return endDate.isBefore(endDateOfValidityPeriod); + return newChallenge; } - } export = GoalInstanceFactory; \ No newline at end of file diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts new file mode 100644 index 0000000..5ac0dba --- /dev/null +++ b/backend/src/challenge/TeamChallenge.ts @@ -0,0 +1,3 @@ +class TeamChallenge { + +} \ No newline at end of file diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index c9e8bb4..99649a0 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -148,8 +148,7 @@ class Goal { } getStringRepresentation():string { - return '\n#' + this.id + '\n' - + '\t' + this.name + '\t-\t' + this.startDate.toISOString() + ' :: ' + this.endDate.toISOString() + '\n' + + return '\n#' + this.id + '\t' + this.name + '\n-\t' + this.startDate.toISOString() + ' :: ' + this.endDate.toISOString() + '\n' + ' - Récurrent : ' + this.recurringSession.getDescription() + '\n' + this.conditionsList.getStringRepresentation(); } } diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index c5e0cdd..c07e45c 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -5,6 +5,7 @@ import ChallengeRepository = require('../challenge/ChallengeRepository'); import Challenge = require('../challenge/Challenge'); import Badge = require('../badge/Badge'); import User = require('../user/User'); +import Team = require('../user/Team'); class GoalDefinitionRepository { @@ -59,7 +60,8 @@ class GoalDefinitionRepository { return null; } - getListOfNotTakenGoalInJSONFormat(user:User, challengeRepository:ChallengeRepository) { + // TODO DELETE OR + getListOfNotTakenGoalInJSONFormat(user:User|Team, challengeRepository:ChallengeRepository) { var result = []; var currentChallengesID:string[] = user.getCurrentChallenges(); diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts index 221f88f..cf60dc8 100644 --- a/backend/src/user/Entity.ts +++ b/backend/src/user/Entity.ts @@ -51,6 +51,10 @@ class Entity { this.name = name; } + public addChallengeFromGoal(goal:Goal) : Challenge { +return null; + } + public addChallenge(challengeID:string):void { if (!challengeID) { throw new Error('Can not add a new goal to user ' + this.getName() + ' given goal is null'); diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 2484094..ce0adfa 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -27,7 +27,9 @@ class Team extends Entity { for (var currentMemberIndex in this.members) { var currentMember = this.members[currentMemberIndex]; - currentMember.addChallenge(challengeID); + + //FIXME + currentMember.addChallenge(null, null); } } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index 018e515..380dc80 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -1,11 +1,141 @@ -import Entity = require('./Entity'); +/// + +/// +/// + +import uuid = require("node-uuid"); +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); +import BadArgumentException = require('../exceptions/BadArgumentException'); + +import Goal = require('../goal/Goal'); +import Challenge = require('../challenge/Challenge'); + +import ChallengeFactory = require('../challenge/ChallengeFactory'); + +class User { + + private id; + private name:string; + private mapSymbolicNameToSensor:any = {}; + private currentChallenges:string[] = []; + private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; + + private challengeFactory:ChallengeFactory; + + constructor(name:string, mapSymbolicNameToSensor:any, currentChallenges:string[], + finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap, challengeFactory:ChallengeFactory, id = null) { + if (name == null) { + throw new BadArgumentException('Can not build user because given name is null'); + } + + if (mapSymbolicNameToSensor == null) { + throw new BadArgumentException('Can not build user ' + name + ' because given map of symbolic name to sensor is null'); + } + + this.id = (id) ? id : uuid.v4(); + + this.name = name; + this.mapSymbolicNameToSensor = mapSymbolicNameToSensor; -class User extends Entity { + this.currentChallenges = (currentChallenges == null) ? [] : currentChallenges; + this.badgesMap = (finishedBadgesMap == null) ? [] : finishedBadgesMap; - constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}) { - super(name, id, currentChallenges, finishedBadgesMap); + this.challengeFactory = challengeFactory; + } + + + getUUID() { + return this.id; + } + + hasUUID(aUUID:string):boolean { + return this.id === aUUID; + } + + setUUID(aUUID:string):void { + this.id = aUUID; + } + + getName():string { + return this.name; + } + + hasName(name:string):boolean { + return this.getName() === name; } -} + getBadges():BadgeIDsToNumberOfTimesEarnedMap { + return this.badgesMap; + } + + public addBadge(badgeID:string) { + if (!badgeID) { + throw new BadArgumentException('Can not add given badge to user' + this.getName() + '. Badge given is null'); + } + + if (this.badgesMap.hasOwnProperty(badgeID)) { + this.badgesMap[badgeID]++; + } else { + this.badgesMap[badgeID] = 1; + } + } + + getCurrentChallenges():string [] { + return this.currentChallenges; + } + + addChallenge(goal:Goal, now:moment.Moment):Challenge { + var newChallenge = this.challengeFactory.createChallenge(goal, this, now); + + // Check if we try + if (newChallenge.getEndDate().isAfter(goal.getEndDate())) { + return null; + } + + this.currentChallenges.push(newChallenge.getId()); + return newChallenge; + } + + deleteChallenge(challengeID:string):void { + + var challengeIndex:number = this.getChallengeByID(challengeID); + if (challengeIndex == -1) { + throw new BadArgumentException('Can not find given challenge ID'); + } + else { + this.currentChallenges.splice(challengeIndex, 1); + } + + console.log("Challenge deleted ! Current challenges:", this.currentChallenges); + } + + private getChallengeByID(challengeID:string):number { + var result:number = -1; + + for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { + if (this.currentChallenges[currentChallengeIndex] === challengeID) { + result = currentChallengeIndex; + } + } + + return result; + } + + getMapSymbolicNameToSensor():any { + return this.mapSymbolicNameToSensor; + } + + public getDataInJSON():any { + return { + id: this.id, + name: this.name, + mapSymbolicNameToSensor: this.mapSymbolicNameToSensor, + currentChallenges: this.currentChallenges, + finishedBadgesMap: this.badgesMap + } + } +} export = User; \ No newline at end of file diff --git a/backend/src/user/UserFactory.ts b/backend/src/user/UserFactory.ts index 5cfa430..901135e 100644 --- a/backend/src/user/UserFactory.ts +++ b/backend/src/user/UserFactory.ts @@ -1,14 +1,17 @@ import Entity = require('./Entity'); import User = require('./User'); +import ChallengeFactory = require('../challenge/ChallengeFactory'); class UserFactory { - public createUser(data:any):Entity { + public createUser(data:any, challengeFactory:ChallengeFactory):User { var userID:string = data.id; var userName:string = data.name; + var mapSymoblicNameToSensor:any = data.mapSymbolicNameToSensor; + var currentChallenges:string[] = data.currentChallenges; var finishedBadgesMap:any = data.finishedBadgesMap; - var user:User = new User(userName, userID, currentChallenges, finishedBadgesMap); + var user:User = new User(userName, mapSymoblicNameToSensor, currentChallenges, finishedBadgesMap, challengeFactory, userID); return user; } } diff --git a/backend/src/user/UserRepository.ts b/backend/src/user/UserRepository.ts index cbeb84d..a27a4cf 100644 --- a/backend/src/user/UserRepository.ts +++ b/backend/src/user/UserRepository.ts @@ -62,7 +62,7 @@ class UserRepository { for (var currentUserIndex in this.users) { var currentUser = this.users[currentUserIndex]; - console.log("#", currentUser.getUUID(), "\t |\tUser : '", currentUser.getName(), "'") + console.log("#", currentUser.getUUID(), "\n\tUser : '", currentUser.getName(), "'\n\t", currentUser.getMapSymbolicNameToSensor()); } } } diff --git a/backend/tests/database/UserStoreTest.ts b/backend/tests/database/UserStoreTest.ts index f47f746..5a46ddd 100644 --- a/backend/tests/database/UserStoreTest.ts +++ b/backend/tests/database/UserStoreTest.ts @@ -46,7 +46,7 @@ describe('Test store user class', () => { }); it('should have the same finished badges map', () => { - chai.expect(userClone.getFinishedBadges()).to.be.eq(finishedBadgesMap); + chai.expect(userClone.getBadges()).to.be.eq(finishedBadgesMap); }); }); }); \ No newline at end of file From 3e8b0ed69496ee23eeb4d6912cc54b2705c55a03 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Fri, 28 Aug 2015 13:05:58 +0200 Subject: [PATCH 10/28] Start : Add teamChallengeFactory and teamChallengeRepo --- backend/db.json | 49 +++++++++-------- backend/src/Backend.ts | 12 ++--- backend/src/Context.ts | 2 +- backend/src/StoringHandler.ts | 3 +- backend/src/api/DashboardRouter.ts | 52 ++++++++++++++++--- backend/src/challenge/TeamChallenge.ts | 26 +++++++++- backend/src/challenge/TeamChallengeFactory.ts | 28 ++++++++++ .../src/challenge/TeamChallengeRepository.ts | 3 ++ .../{Challenge.ts => UserChallenge.ts} | 4 +- ...engeFactory.ts => UserChallengeFactory.ts} | 23 ++------ ...pository.ts => UserChallengeRepository.ts} | 4 +- backend/src/condition/ConditionList.ts | 2 +- backend/src/context/DemoContext.ts | 4 +- backend/src/goal/Goal.ts | 2 +- backend/src/goal/GoalRepository.ts | 4 +- backend/src/user/Entity.ts | 4 +- backend/src/user/Team.ts | 4 ++ backend/src/user/User.ts | 6 +-- backend/src/user/UserFactory.ts | 2 +- .../tests/challenge/ChallengeFactoryTest.ts | 2 +- backend/tests/challenge/ChallengeTest.ts | 2 +- .../integration/ChallengeBuildingTest.ts | 8 +-- .../scripts/controllers/ServiceChallenge.js | 6 +-- 23 files changed, 171 insertions(+), 81 deletions(-) create mode 100644 backend/src/challenge/TeamChallengeFactory.ts create mode 100644 backend/src/challenge/TeamChallengeRepository.ts rename backend/src/challenge/{Challenge.ts => UserChallenge.ts} (99%) rename backend/src/challenge/{ChallengeFactory.ts => UserChallengeFactory.ts} (75%) rename backend/src/challenge/{ChallengeRepository.ts => UserChallengeRepository.ts} (97%) diff --git a/backend/db.json b/backend/db.json index 9d464e0..c85ad27 100644 --- a/backend/db.json +++ b/backend/db.json @@ -123,26 +123,31 @@ "leader":"2cf91e02-a320-4766-aa9f-6efce3142d44" } ], - "challenges": [ - { - "id": "af0947e9-bf85-4233-8d50-2de787bf6021", - "name": "Clim", - "timeProgress": 100, - "startDate": "2015-08-02T22:00:00.000Z", - "endDate": "2015-08-07T21:59:59.999Z", - "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", - "status": "SUCCESS" - }, - { - "id": "f3abd585-b5a2-43d2-bced-738d646921b8", - "name": "Clim", - "timeProgress": 0, - "startDate": "2015-08-09T22:00:00.000Z", - "endDate": "2015-08-14T21:59:59.999Z", - "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", - "status": "WAIT" - } - ] + "challenges": + { + "userChallenges": + [ + { + "id": "af0947e9-bf85-4233-8d50-2de787bf6021", + "name": "Clim", + "timeProgress": 100, + "startDate": "2015-08-02T22:00:00.000Z", + "endDate": "2015-08-07T21:59:59.999Z", + "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", + "status": "SUCCESS" + }, + { + "id": "f3abd585-b5a2-43d2-bced-738d646921b8", + "name": "Clim", + "timeProgress": 0, + "startDate": "2015-08-09T22:00:00.000Z", + "endDate": "2015-08-14T21:59:59.999Z", + "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", + "status": "WAIT" + } + ], + "teamChallenges":[] + } } \ No newline at end of file diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index 96dcdbf..d469163 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -13,8 +13,8 @@ import BadgeFactory = require('./badge/BadgeFactory'); import GoalRepository = require('./goal/GoalRepository'); import GoalFactory = require('./goal/GoalFactory'); -import ChallengeRepository = require('./challenge/ChallengeRepository'); -import ChallengeFactory = require('./challenge/ChallengeFactory'); +import UserChallengeRepository = require('./challenge/UserChallengeRepository'); +import UserChallengeFactory = require('./challenge/UserChallengeFactory'); import UserRepository = require('./user/UserRepository'); import UserFactory = require('./user/UserFactory'); @@ -39,8 +39,8 @@ class Backend extends Server { public goalDefinitionRepository:GoalRepository; public goalDefinitionFactory:GoalFactory; - public challengeRepository:ChallengeRepository; - public challengeFactory:ChallengeFactory; + public challengeRepository:UserChallengeRepository; + public challengeFactory:UserChallengeFactory; public userRepository:UserRepository; public userFactory:UserFactory; @@ -67,8 +67,8 @@ class Backend extends Server { this.goalDefinitionRepository = new GoalRepository(this.badgeRepository); this.goalDefinitionFactory = new GoalFactory(); - this.challengeRepository = new ChallengeRepository(); - this.challengeFactory = new ChallengeFactory(); + this.challengeRepository = new UserChallengeRepository(); + this.challengeFactory = new UserChallengeFactory(); this.userRepository = new UserRepository(); this.userFactory = new UserFactory(); diff --git a/backend/src/Context.ts b/backend/src/Context.ts index 0f92992..b102a18 100644 --- a/backend/src/Context.ts +++ b/backend/src/Context.ts @@ -1,5 +1,5 @@ import GoalRepository = require('./goal/GoalRepository'); -import ChallengeRepository = require('./challenge/ChallengeRepository'); +import ChallengeRepository = require('./challenge/UserChallengeRepository'); import UserRepository = require('./user/UserRepository'); interface Context { diff --git a/backend/src/StoringHandler.ts b/backend/src/StoringHandler.ts index 084f2b1..c2d9c5f 100644 --- a/backend/src/StoringHandler.ts +++ b/backend/src/StoringHandler.ts @@ -30,7 +30,8 @@ class StoringHandler { result['definitions'] = this.backend.goalDefinitionRepository.getDataInJSON(); result['badges'] = this.backend.badgeRepository.getDataInJSON(); result['users'] = this.backend.userRepository.getDataInJSON(); - result['challenges'] = this.backend.challengeRepository.getDataInJSON(); + result['challenges'] = {}; + result['challenges']['userChallenges'] = this.backend.challengeRepository.getDataInJSON(); this.serializer.save(result, successCallBack, failCallBack); } diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index c02c43e..1f21a79 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -1,12 +1,12 @@ import RouterItf = require('./RouterItf'); -import ChallengeRepository = require('../challenge/ChallengeRepository'); -import ChallengeFactory = require('../challenge/ChallengeFactory'); +import ChallengeRepository = require('../challenge/UserChallengeRepository'); +import ChallengeFactory = require('../challenge/UserChallengeFactory'); import GoalRepository = require('../goal/GoalRepository'); import BadgeRepository = require('../badge/BadgeRepository'); import UserRepository = require('../user/UserRepository'); import TeamRepository = require('../user/TeamRepository'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); import Clock = require('../Clock'); import ChallengeStatus = require('../Status'); import Goal = require('../goal/Goal'); @@ -67,6 +67,7 @@ class DashboardRouter extends RouterItf { var result:any = {}; var currentUser:User = self.userRepository.getUser(userID); + var team:Team = self.teamRepository.getTeam(dashboardWanted); if (currentUser == null && team == null) { @@ -300,6 +301,9 @@ class DashboardRouter extends RouterItf { getPersonalDashboard(user:User):any { var result:any = {}; + + console.log('Current client', user.getName()); + console.log("Personal Dashboard mode"); // Evaluate challenge and return them @@ -310,10 +314,10 @@ class DashboardRouter extends RouterItf { var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(user, this.challengeRepository); // Second col : badge description - var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(this.currentUser); + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenUser(user); // Third col : Build the description of updated challenges (potential additions/deletions) - var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(this.currentUser); + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenUser(user); // Build the response result.goals = descriptionOfAvailableGoals; @@ -333,6 +337,7 @@ class DashboardRouter extends RouterItf { this.evaluateChallenge(team, currentChallenge, currentChallengeID); } } + private evaluateChallengeForGivenUser(user:User):void { var challenges = user.getCurrentChallenges(); for (var challengeIndex in challenges) { @@ -345,9 +350,22 @@ class DashboardRouter extends RouterItf { private buildCurrentChallengesDescriptionForGivenEntity(entity:Entity):any[] { var descriptionOfChallenges:any[] = []; + var challenges = entity.getChallenges(); + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + var currentChallengeDesc = currentChallenge.getDataInJSON(); + descriptionOfChallenges.push(currentChallengeDesc); + } + + return descriptionOfChallenges; + } + + + private buildCurrentChallengesDescriptionForGivenUser(user:User):any[] { + var descriptionOfChallenges:any[] = []; - var challenges = entity.getChallenges() - ; + var challenges = user.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); @@ -358,6 +376,7 @@ class DashboardRouter extends RouterItf { return descriptionOfChallenges; } + private buildBadgesDescriptionForGivenEntity(entity:Entity):any[] { var descriptionOfBadges:any[] = []; @@ -376,6 +395,25 @@ class DashboardRouter extends RouterItf { return descriptionOfBadges; } + + private buildBadgesDescriptionForGivenUser(user:User):any[] { + var descriptionOfBadges:any[] = []; + + var badges = user.getBadges(); + + for (var currentBadgeIDIndex in badges) { + var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); + var dataTrophy = { + number: badges[currentBadgeIDIndex], + badge: currentBadge + }; + + descriptionOfBadges.push(dataTrophy); + } + + return descriptionOfBadges; + } + private evaluateChallenge(entity, challengeToEvaluate:Challenge, challengeID) { var self = this; diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index 5ac0dba..a8340bc 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -1,3 +1,27 @@ +import Status= require('../Status'); +import UserChallenge= require('./UserChallenge'); +import Team= require('../user/Team'); + class TeamChallenge { -} \ No newline at end of file + private id:string; + + private team:Team; + + private status:Status; + private childCHallenges:UserChallenge; + private elapsedTime:number; + private progress:any = {}; + + constructor(team:Team, childChallenges:UserChallenge[], id = null) { + + } + + evaluate(data):void { + for(var currentChild in this.childCHallenges) { + //currentChild.evaluate(data); + } + } +} + +export = TeamChallenge; \ No newline at end of file diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts new file mode 100644 index 0000000..e29faec --- /dev/null +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -0,0 +1,28 @@ +import Team = require('../user/Team'); +import User = require('../user/User'); +import Goal = require('../goal/Goal'); +import UserChallenge = require('../challenge/UserChallenge'); +import TeamChallenge = require('../challenge/TeamChallenge'); + +class TeamChallengeFactory { + createTeamChallenge(team:Team, goal:Goal, now) { + + var membersChallenges:UserChallenge[] = []; + + var members:User[] = team.getMembers(); + + for(var currentMemberIndex in members) { + var currentMember:User = members[currentMemberIndex]; + var currentUserChallenge:UserChallenge = currentMember.addChallenge(goal, now); + membersChallenges.push(currentUserChallenge); + } + + return new TeamChallenge(team, membersChallenges); + } + + restoreTeamChallenge() { + + } +} + +export = TeamChallengeFactory \ No newline at end of file diff --git a/backend/src/challenge/TeamChallengeRepository.ts b/backend/src/challenge/TeamChallengeRepository.ts new file mode 100644 index 0000000..08cf7a0 --- /dev/null +++ b/backend/src/challenge/TeamChallengeRepository.ts @@ -0,0 +1,3 @@ +/** + * Created by Benjamin on 28/08/2015. + */ diff --git a/backend/src/challenge/Challenge.ts b/backend/src/challenge/UserChallenge.ts similarity index 99% rename from backend/src/challenge/Challenge.ts rename to backend/src/challenge/UserChallenge.ts index 006740e..fb3471b 100644 --- a/backend/src/challenge/Challenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -14,7 +14,7 @@ import BadgeStatus = require('../Status'); import TimeBox = require('../TimeBox'); import Clock = require('../Clock'); -class Challenge { +class UserChallenge { private id:string; private goal:Goal; @@ -250,4 +250,4 @@ class Challenge { } } -export = Challenge; +export = UserChallenge; diff --git a/backend/src/challenge/ChallengeFactory.ts b/backend/src/challenge/UserChallengeFactory.ts similarity index 75% rename from backend/src/challenge/ChallengeFactory.ts rename to backend/src/challenge/UserChallengeFactory.ts index 6596ed3..36d0ca5 100644 --- a/backend/src/challenge/ChallengeFactory.ts +++ b/backend/src/challenge/UserChallengeFactory.ts @@ -2,7 +2,7 @@ /// /// -import Challenge = require('./Challenge'); +import UserChallenge = require('./UserChallenge'); import User = require('../user/User'); import Goal = require('../goal/Goal'); import GoalRepository = require('../goal/GoalRepository'); @@ -19,7 +19,7 @@ var moment_timezone = require('moment-timezone'); class GoalInstanceFactory { - restoreChallenge(data:any, goalRepository:GoalRepository, userRepository:UserRepository, now:moment.Moment):Challenge { + restoreChallenge(data:any, goalRepository:GoalRepository, userRepository:UserRepository, now:moment.Moment):UserChallenge { var goalID:string = data.goalID; if(goalID == null) { @@ -47,31 +47,18 @@ class GoalInstanceFactory { var startDate = Clock.getMoment(startDateDesc); var endDate = Clock.getMoment(endDateDesc); - var challenge:Challenge = this.createChallenge(goal, user, now, startDate, endDate); - - /* - var challenge:Challenge = new Challenge(startDate, endDate, goalInstanceDescription, goalDefinition, mapGoalsToConditionAndSensors, id); - - if (now.isBefore(startDate)) { - //console.log("Le challenge est en WAIT"); - challenge.setStatus(ChallengeStatus.WAIT); - } - if (now.isAfter(startDate) && now.isBefore(endDate)) { - ///console.log("Le challenge est en RUN"); - challenge.setStatus(ChallengeStatus.RUN); - } - */ + var challenge:UserChallenge = this.createChallenge(goal, user, now, startDate, endDate); return challenge; } - createChallenge(goal:Goal, user:User, now:moment.Moment, startDateSaved = null, endDateSaved = null):Challenge { + createChallenge(goal:Goal, user:User, now:moment.Moment, startDateSaved = null, endDateSaved = null):UserChallenge { var clone = now.clone(); var startDateOfChallenge = (startDateSaved == null) ? goal.getStartDateOfSession(clone) : startDateSaved; var endDateOfChallenge = (endDateSaved == null) ? goal.getEndDateOfSession(clone.clone()) : endDateSaved; - var newChallenge:Challenge = new Challenge(goal, user, startDateOfChallenge, endDateOfChallenge); + var newChallenge:UserChallenge = new UserChallenge(goal, user, startDateOfChallenge, endDateOfChallenge); if (newChallenge.getEndDate().isAfter(goal.getEndDate())) { return null; diff --git a/backend/src/challenge/ChallengeRepository.ts b/backend/src/challenge/UserChallengeRepository.ts similarity index 97% rename from backend/src/challenge/ChallengeRepository.ts rename to backend/src/challenge/UserChallengeRepository.ts index de58190..e36b960 100644 --- a/backend/src/challenge/ChallengeRepository.ts +++ b/backend/src/challenge/UserChallengeRepository.ts @@ -1,5 +1,5 @@ -import Challenge = require('./Challenge'); -import ChallengeFactory = require('./ChallengeFactory'); +import Challenge = require('./UserChallenge'); +import ChallengeFactory = require('./UserChallengeFactory'); import GoalRepository = require('../goal/GoalRepository'); import UserProvider = require('../user/UserRepository'); import Badge = require('../badge/Badge'); diff --git a/backend/src/condition/ConditionList.ts b/backend/src/condition/ConditionList.ts index aa328f2..fc4fd69 100644 --- a/backend/src/condition/ConditionList.ts +++ b/backend/src/condition/ConditionList.ts @@ -7,7 +7,7 @@ import Condition = require('./Condition'); import GoalExpression = require('./expression/GoalExpression'); import TimeBox = require('../TimeBox'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); class ConditionList { diff --git a/backend/src/context/DemoContext.ts b/backend/src/context/DemoContext.ts index 6bc90e5..4d5a002 100644 --- a/backend/src/context/DemoContext.ts +++ b/backend/src/context/DemoContext.ts @@ -3,7 +3,7 @@ import UUID = require('node-uuid'); import Context = require('../Context'); import GoalRepository = require('../goal/GoalRepository'); -import ChallengeRepository = require('../challenge/ChallengeRepository'); +import ChallengeRepository = require('../challenge/UserChallengeRepository'); import UserRepository = require('../user/UserRepository'); import Goal = require('../goal/Goal'); @@ -11,7 +11,7 @@ import GoalExpression = require('../condition/expression/GoalExpression'); import Operand = require('../condition/expression/Operand'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); class DemoContext implements Context { diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index 99649a0..3864241 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -11,7 +11,7 @@ import uuid = require('node-uuid'); import ConditionList = require('../condition/ConditionList'); import Condition = require('../condition/Condition'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); import TimeBox = require('../TimeBox'); import Clock = require('../Clock'); import RecurringSession = require('./RecurringSession'); diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index c07e45c..779a6bc 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -1,8 +1,8 @@ import Goal = require('./Goal'); import GoalFactory = require('./GoalFactory'); import BadgeRepository = require('../badge/BadgeRepository'); -import ChallengeRepository = require('../challenge/ChallengeRepository'); -import Challenge = require('../challenge/Challenge'); +import ChallengeRepository = require('../challenge/UserChallengeRepository'); +import Challenge = require('../challenge/UserChallenge'); import Badge = require('../badge/Badge'); import User = require('../user/User'); import Team = require('../user/Team'); diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts index cf60dc8..2aea9f0 100644 --- a/backend/src/user/Entity.ts +++ b/backend/src/user/Entity.ts @@ -1,7 +1,7 @@ import uuid = require('node-uuid'); import Goal = require('../goal/Goal'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); import Badge = require('../badge/Badge'); import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); @@ -73,7 +73,7 @@ return null; this.currentChallenges.splice(challengeIndex, 1); } - console.log("Challenge deleted ! Current challenges:", this.currentChallenges); + console.log("UserChallenge deleted ! Current challenges:", this.currentChallenges); } public getChallenge(challengeID:string):number { diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index ce0adfa..32c87d4 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -13,6 +13,10 @@ class Team extends Entity { this.leader = leader; } + getMembers():User[] { + return this.members; + } + public hasLeader(aUserID:string):boolean { return this.leader.hasUUID(aUserID); } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index 380dc80..e382de4 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -11,9 +11,9 @@ import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarn import BadArgumentException = require('../exceptions/BadArgumentException'); import Goal = require('../goal/Goal'); -import Challenge = require('../challenge/Challenge'); +import Challenge = require('../challenge/UserChallenge'); -import ChallengeFactory = require('../challenge/ChallengeFactory'); +import ChallengeFactory = require('../challenge/UserChallengeFactory'); class User { @@ -109,7 +109,7 @@ class User { this.currentChallenges.splice(challengeIndex, 1); } - console.log("Challenge deleted ! Current challenges:", this.currentChallenges); + console.log("UserChallenge deleted ! Current challenges:", this.currentChallenges); } private getChallengeByID(challengeID:string):number { diff --git a/backend/src/user/UserFactory.ts b/backend/src/user/UserFactory.ts index 901135e..a115892 100644 --- a/backend/src/user/UserFactory.ts +++ b/backend/src/user/UserFactory.ts @@ -1,6 +1,6 @@ import Entity = require('./Entity'); import User = require('./User'); -import ChallengeFactory = require('../challenge/ChallengeFactory'); +import ChallengeFactory = require('../challenge/UserChallengeFactory'); class UserFactory { public createUser(data:any, challengeFactory:ChallengeFactory):User { diff --git a/backend/tests/challenge/ChallengeFactoryTest.ts b/backend/tests/challenge/ChallengeFactoryTest.ts index 63c426f..8124596 100644 --- a/backend/tests/challenge/ChallengeFactoryTest.ts +++ b/backend/tests/challenge/ChallengeFactoryTest.ts @@ -12,7 +12,7 @@ import chai = require('chai'); import sinon = require('sinon'); var assert = chai.assert; -import ChallengeFactory = require('../../src/challenge/ChallengeFactory'); +import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); import GoalRepository = require('../../src/goal/GoalRepository'); import Goal = require('../../src/goal/Goal'); import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); diff --git a/backend/tests/challenge/ChallengeTest.ts b/backend/tests/challenge/ChallengeTest.ts index 2b77684..97acb28 100644 --- a/backend/tests/challenge/ChallengeTest.ts +++ b/backend/tests/challenge/ChallengeTest.ts @@ -13,7 +13,7 @@ import chai = require('chai'); import sinon = require('sinon'); var assert = chai.assert; -import Challenge = require('../../src/challenge/Challenge'); +import Challenge = require('../../src/challenge/UserChallenge'); import Goal = require('../../src/goal/Goal'); import GoalExpression = require('../../src/condition/expression/GoalExpression'); import Operand = require('../../src/condition/expression/Operand'); diff --git a/backend/tests/integration/ChallengeBuildingTest.ts b/backend/tests/integration/ChallengeBuildingTest.ts index 48733f9..fbcadec 100644 --- a/backend/tests/integration/ChallengeBuildingTest.ts +++ b/backend/tests/integration/ChallengeBuildingTest.ts @@ -12,9 +12,9 @@ var assert = chai.assert; import BadgeRepository = require('../../src/badge/BadgeRepository'); import Badge = require('../../src/badge/Badge'); -import ChallengeRepository = require('../../src/challenge/ChallengeRepository'); -import ChallengeFactory = require('../../src/challenge/ChallengeFactory'); -import Challenge = require('../../src/challenge/Challenge'); +import ChallengeRepository = require('../../src/challenge/UserChallengeRepository'); +import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); +import Challenge = require('../../src/challenge/UserChallenge'); import GoalRepository = require('../../src/goal/GoalRepository'); import Goal = require('../../src/goal/Goal'); @@ -32,7 +32,7 @@ import Middleware = require('../../src/Middleware'); import DashboardRouter = require('../../src/api/DashboardRouter'); -describe('Challenge integration test', () => { +describe('UserChallenge integration test', () => { // Important ! Allow us to set time DashboardRouter.DEMO = true; diff --git a/frontend/app/scripts/controllers/ServiceChallenge.js b/frontend/app/scripts/controllers/ServiceChallenge.js index 40b5821..4a7b285 100644 --- a/frontend/app/scripts/controllers/ServiceChallenge.js +++ b/frontend/app/scripts/controllers/ServiceChallenge.js @@ -1,7 +1,7 @@ 'use strict'; /** - * File with all the services associated to Challenge (GET, POST) + * File with all the services associated to UserChallenge (GET, POST) */ var basePath = 'http://localhost:3000/challenges/'; @@ -10,7 +10,7 @@ var app = angular.module('ecoknowledgeApp'); app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { this.get = function (id, successFunc, failFunc) { var path = basePath + 'all/' + id; - console.log('Service Challenge : Get On ', path); + console.log('Service UserChallenge : Get On ', path); $http.get(path) .success(function (data) { @@ -43,7 +43,7 @@ app.service('ServiceChallenge', ['$http', function ServiceChallenge($http) { this.evaluate = function (badgeName, successFunc, failFunc) { var path = basePath + 'evaluate/' + (badgeName === 'all' ? 'all' : ('evaluatebadge?badgeName=' + badgeName)); - console.log('Service Challenge : Get On ', path); + console.log('Service UserChallenge : Get On ', path); $http.get(path) .success(function (data) { From e802088b331cc3fc75944ae10d44219b73c1ff2f Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Fri, 28 Aug 2015 13:43:35 +0200 Subject: [PATCH 11/28] Add Team implementation Entity class become useless, that's the point of it. Seperate clearly User and Team ; and UserChallenge and TeamChallenge and do the refactor after --- backend/src/user/Entity.ts | 18 +++--- backend/src/user/Team.ts | 109 ++++++++++++++++++++++++++++--------- 2 files changed, 91 insertions(+), 36 deletions(-) diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts index 2aea9f0..6c7df78 100644 --- a/backend/src/user/Entity.ts +++ b/backend/src/user/Entity.ts @@ -23,36 +23,36 @@ class Entity { this.finishedBadgesMap = finishedBadgesMap; } - getCurrentChallenges():string [] { + getCurrentChallenges():string [] { return this.currentChallenges; } - public getUUID() { + getUUID() { return this.id; } - public hasUUID(aUUID:string):boolean { + hasUUID(aUUID:string):boolean { return this.id === aUUID; } - public setUUID(aUUID:string):void { + setUUID(aUUID:string):void { this.id = aUUID; } - public getName():string { + getName():string { return this.name; } - public hasName(name:string):boolean { + hasName(name:string):boolean { return this.getName() === name; } - public setName(name:string):void { + setName(name:string):void { this.name = name; } - public addChallengeFromGoal(goal:Goal) : Challenge { -return null; + public addChallengeFromGoal(goal:Goal):Challenge { + return null; } public addChallenge(challengeID:string):void { diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 32c87d4..3d95f57 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -1,52 +1,81 @@ +import uuid = require('node-uuid'); +import BadArgumentException = require('../exceptions/BadArgumentException'); + import Entity = require('./Entity'); import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); import User = require('./User'); -class Team extends Entity { +class Team { + private id:string; + private name:string; + private members:User[] = []; private leader:User; - constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}, members:User[] = [], leader:User = null) { - super(name, id, currentChallenges, finishedBadgesMap); + private currentChallenges:string[] = []; + private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; + + constructor(name:string, leader:User, members:User[], currentChallenges:string[], badgesMap:BadgeIDsToNumberOfTimesEarnedMap, id = null) { + + if (name == null) { + throw new BadArgumentException('Can not build team, given name is null'); + } + + if (leader == null) { + throw new BadArgumentException('Can not build team ' + name + ' given leader is null'); + } + + if (members == null) { + throw new BadArgumentException('Can not build team ' + name + ' given members is null'); + } + + if (currentChallenges == null) { + throw new BadArgumentException('Can not build team ' + name + ' given current challenges are null'); + } + + if (badgesMap == null) { + throw new BadArgumentException('Can not build team ' + name + ' given badges map is null'); + } + + this.id = (id == null) ? uuid.v4() : id; + this.name = name; - this.members = members; this.leader = leader; + this.members = members; + + this.currentChallenges = currentChallenges; + this.badgesMap = badgesMap; } - getMembers():User[] { - return this.members; + getUUID() { + return this.id; } - public hasLeader(aUserID:string):boolean { - return this.leader.hasUUID(aUserID); + getName():string { + return this.name; } - public getLeader() : User { + getLeader():User { return this.leader; } - public addChallenge(challengeID:string):void { - - super.addChallenge(challengeID); - - for (var currentMemberIndex in this.members) { - var currentMember = this.members[currentMemberIndex]; + getMembers():User[] { + return this.members; + } - //FIXME - currentMember.addChallenge(null, null); - } + hasUUID(aUUID:string):boolean { + return this.id === aUUID; } - public deleteChallenge(challengeID:string):void { - super.deleteChallenge(challengeID); + hasName(name:string):boolean { + return this.getName() === name; + } - for (var currentMemberIndex in this.members) { - var currentMember = this.members[currentMemberIndex]; - currentMember.deleteChallenge(challengeID); - } + hasLeader(aUserID:string):boolean { + return this.leader.hasUUID(aUserID); } - public hasMember(aUserID:string):boolean { + hasMember(aUserID:string):boolean { for (var currentMemberIndex in this.members) { var currentMember = this.members[currentMemberIndex]; if (currentMember.hasUUID(aUserID)) { @@ -57,11 +86,37 @@ class Team extends Entity { return false; } - public getStringDescription():string { + addChallenge(challengeID:string):void { + this.currentChallenges.push(challengeID); + } + + deleteChallenge(challengeID:string):void { + var challengeIndex:number = this.getChallenge(challengeID); + if (challengeIndex == -1) { + throw new BadArgumentException('Can not find given challenge ID'); + } + else { + this.currentChallenges.splice(challengeIndex, 1); + } + } + + getChallenge(challengeID:string):number { + var result:number = -1; + + for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { + if (this.currentChallenges[currentChallengeIndex] === challengeID) { + result = currentChallengeIndex; + } + } + + return result; + } + + getStringDescription():string { return 'Team:#' + this.getUUID() + '\t|Name : ' + this.getName() + '\t|LEADER : ' + this.leader + '\n'; } - public getStringDescriptionOfMembers() : string { + getStringDescriptionOfMembers():string { var result = ''; for (var currentMemberIndex in this.members) { From d9086a1b47ba40d956967a1843033a8b3219dfd8 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Fri, 28 Aug 2015 14:56:58 +0200 Subject: [PATCH 12/28] Add TeamChallenge implementation Add TeamChallenge class, add class logic and update userChallenge class Keep separating user and team before refactoring code into single entity class --- backend/src/api/DashboardRouter.ts | 8 ++-- backend/src/challenge/TeamChallenge.ts | 55 +++++++++++++++++++++-- backend/src/challenge/UserChallenge.ts | 62 ++++++++++++++++---------- backend/src/condition/ConditionList.ts | 3 ++ backend/src/user/Team.ts | 7 +++ 5 files changed, 105 insertions(+), 30 deletions(-) diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 1f21a79..4531e8a 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -348,9 +348,9 @@ class DashboardRouter extends RouterItf { } } - private buildCurrentChallengesDescriptionForGivenEntity(entity:Entity):any[] { + private buildCurrentChallengesDescriptionForGivenEntity(team:Team):any[] { var descriptionOfChallenges:any[] = []; - var challenges = entity.getChallenges(); + var challenges = team.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); @@ -377,10 +377,10 @@ class DashboardRouter extends RouterItf { } - private buildBadgesDescriptionForGivenEntity(entity:Entity):any[] { + private buildBadgesDescriptionForGivenEntity(team:Team):any[] { var descriptionOfBadges:any[] = []; - var badges = entity.getFinishedBadges(); + var badges = team.getBadges(); for (var currentBadgeIDIndex in badges) { var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index a8340bc..2c2c914 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -1,26 +1,75 @@ +import uuid = require('node-uuid'); +var merge = require('merge'); + import Status= require('../Status'); import UserChallenge= require('./UserChallenge'); import Team= require('../user/Team'); +import BadArgumentException = require('../exceptions/BadArgumentException'); class TeamChallenge { private id:string; private team:Team; + private childChallenges:UserChallenge[]; private status:Status; - private childCHallenges:UserChallenge; private elapsedTime:number; private progress:any = {}; constructor(team:Team, childChallenges:UserChallenge[], id = null) { + if (childChallenges.length == 0) { + throw new BadArgumentException('Can not build team challenge because there is no child challenges to attach'); + } + + this.id = (id == null) ? uuid.v4() : id; + + this.team = team; + this.childChallenges = childChallenges; + this.status = this.childChallenges[0].getStatus(); } evaluate(data):void { - for(var currentChild in this.childCHallenges) { - //currentChild.evaluate(data); + + var childProgress:number = 0; + + for (var currentChildIndex in this.childChallenges) { + var currentChild:UserChallenge = this.childChallenges[currentChildIndex]; + currentChild.evaluate(data); + var currentChildGlobalProgression:number = currentChild.getGlobalProgression(); + childProgress += currentChildGlobalProgression; + + var currentChildProgressionDescription:any = { + name: currentChild.getName(), + progression: currentChildGlobalProgression + }; + + this.progress[currentChild.getId()] = currentChildProgressionDescription; + } + this.elapsedTime = this.childChallenges[0].getTimeProgress(); + } + + getSensors() { + /* + Precisions : + We can not assume that a specific sensor is bound to a specific user + since two users can share an office. + But we can assume that for a given challenge, sensor of each user + have the same timeBox and will be identical. + Because of this, we can simply merge required sensors and not + try to merge different timeBoxes or whatever. + */ + + var result:any = {}; + + for (var currentChildIndex in this.childChallenges) { + var currentChild:UserChallenge = this.childChallenges[currentChildIndex]; + result = merge(result, currentChild.getSensors()); + } + + return result; } } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index fb3471b..d375ed9 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -25,6 +25,7 @@ class UserChallenge { private status:BadgeStatus; private progress:any[] = []; + private progressDescription:any = {}; private percentageOfTime:number = 0; // { 'tmp_cli':'ac_443', 'tmp_ext':'TEMP_444', 'door_o':'D_55', ... } @@ -32,8 +33,8 @@ class UserChallenge { private user:User; - constructor(goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, description:string='', - mapGoalToConditionAndSensor:any={}, id = null) { + constructor(goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, description:string = '', + mapGoalToConditionAndSensor:any = {}, id = null) { this.id = (id) ? id : UUID.v4(); this.description = description; @@ -71,68 +72,83 @@ class UserChallenge { return this.getTimeProgress() >= 100; } - public getTimeProgress():number { + getTimeProgress():number { return this.percentageOfTime; } - public resetProgress() { + resetProgress() { this.progress = []; } - public addProgress(progressDescription:any) { + // TODO delete following method + addProgress(progressDescription:any) { this.progress.push(progressDescription); } - public getStartDate():moment.Moment { + addProgressByCondition(conditionID:string, percentageAchieved:number) { + this.progressDescription[conditionID] = percentageAchieved; + } + + getGlobalProgression():number { + var globalProgression:number = 0; + + for (var currentConditionID in this.progressDescription) { + var currentConditionProgression = this.progressDescription[currentConditionID]; + globalProgression += currentConditionProgression; + } + + return globalProgression / (Object.keys(this.progressDescription)).length; + } + + getStartDate():moment.Moment { return this.startDate; } - public getEndDate():moment.Moment { + getEndDate():moment.Moment { return this.endDate; } - public getDescription():string { + getDescription():string { return this.description; } - public getGoalDefinition():Goal { + getGoalDefinition():Goal { return this.goal; } - public getBadge():string { + getBadge():string { return this.goal.getBadgeID(); } - public getName():string { + getName():string { return this.goal.getName(); } - public getId():string { + getId():string { return this.id; } - public hasUUID(aUUID:string):boolean { + hasUUID(aUUID:string):boolean { return this.id === aUUID; } - public getProgress():any { - console.log("PROGRESS", JSON.stringify(this.progress)); + getProgress():any { return this.progress; } - public getStatus():BadgeStatus { + getStatus():BadgeStatus { return this.status; } - public hasStatus(badgeStatus:BadgeStatus):boolean { + hasStatus(badgeStatus:BadgeStatus):boolean { return this.status === badgeStatus; } - public setStatus(badgeStatus:BadgeStatus) { + setStatus(badgeStatus:BadgeStatus) { this.status = badgeStatus; } - public getSensors():any { + getSensors():any { var result:any = {}; @@ -148,7 +164,7 @@ class UserChallenge { this.goal = goal; } - public haveToStart(now:moment.Moment):boolean { + haveToStart(now:moment.Moment):boolean { return now.isAfter(this.startDate) && now.isBefore(this.endDate); } @@ -164,7 +180,7 @@ class UserChallenge { * } * @returns {boolean} */ - public evaluate(values:any):boolean { + evaluate(values:any):boolean { console.log('evaluate de challenge'); // Check if badge is running. If Waiting or failed, it must be left unchanged if (this.status != BadgeStatus.RUN) { @@ -201,7 +217,7 @@ class UserChallenge { return false; } - public bindSymbolicNameToValue(mapSensorToValue:any) { + bindSymbolicNameToValue(mapSensorToValue:any) { var result:any = {}; for (var currentSymbolicName in this.mapSymbolicNameToSensor) { @@ -214,7 +230,7 @@ class UserChallenge { return result; } - public getDataInJSON():any { + getDataInJSON():any { console.log('time progress : ', this.percentageOfTime); return { id: this.id, diff --git a/backend/src/condition/ConditionList.ts b/backend/src/condition/ConditionList.ts index fc4fd69..3fbe930 100644 --- a/backend/src/condition/ConditionList.ts +++ b/backend/src/condition/ConditionList.ts @@ -118,7 +118,10 @@ class ConditionList { var conditionDescription:any = this.conditions[i].getDataInJSON(); if (challenge != null) { + + // TODO delete following call challenge.addProgress(conditionDescription); + challenge.addProgressByCondition(this.conditions[i].getID(), this.conditions[i].getPercentageAchieved()); } } diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 3d95f57..9ff2fdc 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -63,6 +63,9 @@ class Team { return this.members; } + getBadges():BadgeIDsToNumberOfTimesEarnedMap { + return this.badgesMap; + } hasUUID(aUUID:string):boolean { return this.id === aUUID; } @@ -112,6 +115,10 @@ class Team { return result; } + getCurrentChallenges():string[] { + return this.currentChallenges; + } + getStringDescription():string { return 'Team:#' + this.getUUID() + '\t|Name : ' + this.getName() + '\t|LEADER : ' + this.leader + '\n'; } From 5d9ad964c4147787f7ccba8b67c00870fd3e9f14 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Mon, 31 Aug 2015 11:21:48 +0200 Subject: [PATCH 13/28] Add condition test Add tests of base class methods --- backend/tests/condition/ConditionTest.ts | 153 +++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 backend/tests/condition/ConditionTest.ts diff --git a/backend/tests/condition/ConditionTest.ts b/backend/tests/condition/ConditionTest.ts new file mode 100644 index 0000000..4fd3fb3 --- /dev/null +++ b/backend/tests/condition/ConditionTest.ts @@ -0,0 +1,153 @@ +/// +/// +/// + +import chai = require('chai'); +import sinon = require('sinon'); +var assert = chai.assert; + + +import Condition = require('../../src/condition/Condition'); +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); +import Filter = require('../../src/filter/Filter'); +import GoalExpression = require('../../src/condition/expression/GoalExpression'); +import Clock = require('../../src/Clock'); + + +describe('Test Condition', () => { + var condition:Condition; + var conditionDescription:any = { + symbolicNames: ["TMP_CLI"], + timeBox: { + start: Clock.getMomentFromString("2000-01-01T00:00:00"), + end: Clock.getMomentFromString("2000-08-01T00:00:00") + } + }; + + var aSymbolicName = "TMP_CLI"; + var aValue = "15"; + var aComparison = ">"; + var expression:GoalExpression; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: aValue, + symbolicName: false + }, + comparison: aComparison + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + expression = expressionFactory.createExpression(expressionDescription); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 80; + var filterOfCondition:Filter = new Filter('all', ['all']); + + condition = new Condition(aConditionID, aConditionDescription, expression, aThresholdRate, filterOfCondition); + + describe('Build test', () => { + + var dataInJSON:any = condition.getDataInJSON(); + + it('should have proper id', () => { + chai.expect(dataInJSON.id).to.be.eq(aConditionID); + }); + + it('should have proper description', () => { + chai.expect(dataInJSON.description).to.be.eq(aConditionDescription); + }); + + it('should have proper threshold', () => { + chai.expect(dataInJSON.threshold).to.be.eq(aThresholdRate); + }); + + it('should have proper threshold', () => { + var expectedFilters:any = { + dayOfWeekFilter: 'all', + periodOfDayFilter: ['all'] + } + + chai.expect(dataInJSON.filter).to.be.eqls(expectedFilters); + }); + + it('should have proper expression', () => { + var expected:any = { + valueLeft: { + value: aSymbolicName, + sensor: true + }, + valueRight: { + value: aValue, + sensor: false + }, + comparison: aComparison + }; + + chai.expect(dataInJSON.expression).to.be.eqls(expected); + }); + }); + + describe('KeepUsefulValues method', () => { + it('should keep nothing if nothing is in the timeBox', () => { + var expected:any[] = []; + var data:any[] = [ + {date: "2000-09-01T00:00:00", value: 10}, + {date: "2000-09-01T00:00:00", value: 10}, + {date: "2000-09-01T00:00:00", value: 10}, + {date: "2000-09-01T00:00:00", value: 10} + ]; + var result:any[] = condition.keepUsefulValues(data, conditionDescription); + + chai.expect(result).to.be.eqls(expected); + }); + + it('should keep everything if everything is in the timeBox', () => { + var expected:any[] = []; + var data:any[] = [ + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10} + ]; + var result:any[] = condition.keepUsefulValues(data, conditionDescription); + + chai.expect(result).to.be.eqls(expected); + }); + + it('should keep what is in the timeBox', () => { + var expected:any[] = []; + var data:any[] = [ + {date: "1999-01-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-09-01T00:00:00", value: 10}, + ]; + var result:any[] = condition.keepUsefulValues(data, conditionDescription); + + chai.expect(result).to.be.eqls(expected); + }); + }); + + describe('GetRequired chain', () => { + it('should have proper required result', () => { + var aStart:moment.Moment = Clock.getMomentFromString("2000-01-01T00:00:00"); + var anEnd:moment.Moment = Clock.getMomentFromString("2000-07-01T00:00:00"); + var expected:any = { + symbolicNames: [aSymbolicName], + timeBox: { + start: aStart, + end: anEnd + } + }; + var result = condition.getRequiredByCondition(aStart, anEnd, null); + chai.expect(result).to.be.eqls(expected); + }); + }); +}); From 99838c6fb0683ba2039c1b9ec32f55de2b9f70a8 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Mon, 31 Aug 2015 17:38:21 +0200 Subject: [PATCH 14/28] Everything compile/ This is the worst commit ever. And tests dont pass. --- backend/src/api/DashboardRouter.ts | 8 +- backend/src/challenge/UserChallenge.ts | 80 ++- backend/src/challenge/UserChallengeFactory.ts | 101 +++- .../src/challenge/UserChallengeRepository.ts | 2 +- backend/src/condition/AverageOnValue.ts | 153 ++--- backend/src/condition/Condition.ts | 198 +++---- backend/src/condition/ConditionList.ts | 154 ----- backend/src/condition/OverallGoalCondition.ts | 66 +-- backend/src/condition/ReferencePeriod.ts | 25 + .../condition/expression/GoalExpression.ts | 12 +- .../src/condition/factory/ConditionFactory.ts | 17 +- .../condition/factory/ExpressionFactory.ts | 63 +- backend/src/filter/Filter.ts | 6 +- backend/src/goal/Goal.ts | 144 ++--- backend/src/goal/GoalFactory.ts | 79 ++- backend/src/goal/GoalRepository.ts | 17 +- backend/src/user/Entity.ts | 44 +- backend/src/user/Team.ts | 86 +-- backend/src/user/TeamFactory.ts | 28 +- backend/src/user/TeamRepository.ts | 10 +- backend/src/user/User.ts | 4 +- backend/tests/TeamTest.ts | 27 +- backend/tests/UserTest.ts | 74 +-- .../tests/challenge/ChallengeFactoryTest.ts | 122 ---- backend/tests/challenge/ChallengeTest.ts | 115 ---- .../challenge/UserChallengeFactoryTest.ts | 124 ++++ backend/tests/challenge/UserChallengeTest.ts | 98 ++++ backend/tests/condition/AverageOnValueTest.ts | 553 ++++++++---------- backend/tests/condition/ConditionTest.ts | 29 +- .../condition/OverallGoalConditionTest.ts | 161 ++--- .../expression/GoalExpressionTest.ts | 36 +- .../tests/database/GoalConditionStoreTest.ts | 9 +- backend/tests/database/UserStoreTest.ts | 11 +- backend/tests/goal/GoalFactoryTest.ts | 9 +- backend/tests/goal/GoalTest.ts | 121 ++-- .../integration/ChallengeBuildingTest.ts | 3 + 36 files changed, 1297 insertions(+), 1492 deletions(-) delete mode 100644 backend/src/condition/ConditionList.ts create mode 100644 backend/src/condition/ReferencePeriod.ts delete mode 100644 backend/tests/challenge/ChallengeFactoryTest.ts delete mode 100644 backend/tests/challenge/ChallengeTest.ts create mode 100644 backend/tests/challenge/UserChallengeFactoryTest.ts create mode 100644 backend/tests/challenge/UserChallengeTest.ts diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 4531e8a..a907f00 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -380,7 +380,7 @@ class DashboardRouter extends RouterItf { private buildBadgesDescriptionForGivenEntity(team:Team):any[] { var descriptionOfBadges:any[] = []; - var badges = team.getBadges(); + var badges = team.getBadgesID(); for (var currentBadgeIDIndex in badges) { var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); @@ -437,7 +437,7 @@ class DashboardRouter extends RouterItf { function () { var result = challengeToEvaluate.evaluate(required); if (result) { - var newChall = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + var newChall = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); this.addBadge(challengeID, entity.getUUID()); if (newChall != null) { self.evaluateChallenge(entity, newChall, newChall.getId()); @@ -470,7 +470,7 @@ class DashboardRouter extends RouterItf { this.addFinishedBadge(challengeID, entity.getUUID()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } @@ -483,7 +483,7 @@ class DashboardRouter extends RouterItf { entity.deleteChallenge(challengeToEvaluate.getId()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoalDefinition().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index d375ed9..290a673 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -5,8 +5,6 @@ var moment = require('moment'); var moment_timezone = require('moment-timezone'); -import UUID = require('node-uuid'); - import Goal = require('../goal/Goal'); import Badge = require('../badge/Badge'); import User = require('../user/User'); @@ -21,35 +19,38 @@ class UserChallenge { private startDate:moment.Moment; private endDate:moment.Moment; - private description:string; private status:BadgeStatus; private progress:any[] = []; private progressDescription:any = {}; private percentageOfTime:number = 0; + // { A_CONDITION_ID : { symbolic_name: tmp_cli, timeBox: { startDate:..., endDate } } } + private mapConditionIDToSensorAndTimeBoxRequired:any = {}; + + // { 'tmp_cli':'ac_443', 'tmp_ext':'TEMP_444', 'door_o':'D_55', ... } private mapSymbolicNameToSensor:any = {}; - private user:User; - constructor(goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, description:string = '', - mapGoalToConditionAndSensor:any = {}, id = null) { - this.id = (id) ? id : UUID.v4(); - this.description = description; + constructor(id:string, goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, mapConditionIDToSensorAndTimeBoxRequired:any) { + + this.id = id; this.startDate = startDate; this.endDate = endDate; this.goal = goal; - // TODO DELETE THIS TOKEN - this.goal.setTimeBoxes(new TimeBox(startDate, endDate)); - this.mapSymbolicNameToSensor = mapGoalToConditionAndSensor; this.user = user; - this.status = BadgeStatus.RUN; + this.mapConditionIDToSensorAndTimeBoxRequired = mapConditionIDToSensorAndTimeBoxRequired; + this.mapSymbolicNameToSensor = this.user.getMapSymbolicNameToSensor(); + } + + getConditionDescriptionByID(conditionID:string) { + return this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; } public updateDurationAchieved(currentDate:number) { @@ -68,6 +69,10 @@ class UserChallenge { this.percentageOfTime = (this.percentageOfTime > 100) ? 100 : this.percentageOfTime; } + getUser():User { + return this.user; + } + isFinished():boolean { return this.getTimeProgress() >= 100; } @@ -80,11 +85,6 @@ class UserChallenge { this.progress = []; } - // TODO delete following method - addProgress(progressDescription:any) { - this.progress.push(progressDescription); - } - addProgressByCondition(conditionID:string, percentageAchieved:number) { this.progressDescription[conditionID] = percentageAchieved; } @@ -108,11 +108,7 @@ class UserChallenge { return this.endDate; } - getDescription():string { - return this.description; - } - - getGoalDefinition():Goal { + getGoal():Goal { return this.goal; } @@ -133,6 +129,7 @@ class UserChallenge { } getProgress():any { + this.progress['global'] = this.getGlobalProgression(); return this.progress; } @@ -150,14 +147,22 @@ class UserChallenge { getSensors():any { - var result:any = {}; + for(var conditionID in this.mapConditionIDToSensorAndTimeBoxRequired) { + var sensors:string[] = []; - for (var currentSymbolicName in this.mapSymbolicNameToSensor) { - var currentSensor = this.mapSymbolicNameToSensor[currentSymbolicName]; - result[currentSensor] = this.goal.getRequired()[currentSymbolicName]; + var currentConditionDescription = this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; + var symbolicNames:string[] = currentConditionDescription.symbolicNames; + for(var symbolicNamesIndex in symbolicNames) { + var currentSymbolicName = symbolicNames[symbolicNamesIndex]; + var currentSensor = this.mapSymbolicNameToSensor[currentSymbolicName]; + sensors.push(currentSensor); + currentConditionDescription[currentSensor] = {}; + } + + currentConditionDescription.sensors = sensors; } - return result; + return this.mapConditionIDToSensorAndTimeBoxRequired; } setGoal(goal) { @@ -201,6 +206,20 @@ class UserChallenge { var mapSymbolicNameToValue = this.bindSymbolicNameToValue(values); + + // TODO + // Il faut ajouter une indirection + // MapSymbolicNameTOValue ne fait que TMP_CLI => [val1, val2] + // Il faut en fait faire CONDITION_ID => { TMP_CLI => [val1] } + // Seul moyen pour que ça fonctionne ! + // Le merge de timebox s'est fait ; il faut rajouter un + // "isInTimeBox" dans bindSNTV pour reconstuire le schéma + // cID => SN => Vals + + + + + var resultEval = this.goal.evaluate(mapSymbolicNameToValue, this); if (resultEval && this.percentageOfTime >= 100) { @@ -230,17 +249,16 @@ class UserChallenge { return result; } + + getDataInJSON():any { console.log('time progress : ', this.percentageOfTime); return { id: this.id, - name: this.getName(), - timeProgress: this.percentageOfTime, startDate: this.startDate, endDate: this.endDate, goal: this.goal.getUUID(), - user: this.user.getUUID(), - status: this.getStatusAsString() + user: this.user.getUUID() } } diff --git a/backend/src/challenge/UserChallengeFactory.ts b/backend/src/challenge/UserChallengeFactory.ts index 36d0ca5..e3752b0 100644 --- a/backend/src/challenge/UserChallengeFactory.ts +++ b/backend/src/challenge/UserChallengeFactory.ts @@ -16,30 +16,22 @@ import BadArgumentException = require('../exceptions/BadArgumentException'); var moment = require('moment'); var moment_timezone = require('moment-timezone'); +import UUID = require('node-uuid'); class GoalInstanceFactory { restoreChallenge(data:any, goalRepository:GoalRepository, userRepository:UserRepository, now:moment.Moment):UserChallenge { - var goalID:string = data.goalID; - if(goalID == null) { - throw new BadArgumentException('Can not restore given challenge because goalID is null'); - } + this.checkDataFromRestore(data, goalRepository, userRepository); + + var challengeID:string = data.id; + challengeID = (challengeID == null) ? UUID.v4() : challengeID; + var goalID:string = data.goalID; var userID:string = data.userID; - if(userID == null) { - throw new BadArgumentException('Can not restore given challenge because userID is null'); - } var goal:Goal = goalRepository.getGoal(goalID); - if(goal == null) { - throw new BadArgumentException('Can not restore given challenge because goal with id ' + goalID + ' was not found'); - } - var user:User = userRepository.getUser(userID); - if(user == null) { - throw new BadArgumentException('Can not restore given challenge because user with id ' + userID + ' was not found'); - } var startDateDesc = data.startDate; var endDateDesc = data.endDate; @@ -47,31 +39,94 @@ class GoalInstanceFactory { var startDate = Clock.getMoment(startDateDesc); var endDate = Clock.getMoment(endDateDesc); - var challenge:UserChallenge = this.createChallenge(goal, user, now, startDate, endDate); + var mapConditionIDToSensorAndTimeBoxRequired:any = data.mapConditionIDToSensorAndTimeBoxRequired; + + var challenge:UserChallenge = this.newChallenge(challengeID, goal, user, mapConditionIDToSensorAndTimeBoxRequired, startDate, endDate, now); return challenge; } - createChallenge(goal:Goal, user:User, now:moment.Moment, startDateSaved = null, endDateSaved = null):UserChallenge { + createChallenge(goal:Goal, user:User, now:moment.Moment):UserChallenge { + this.checkDataFromCreate(goal, user, now); + + var challengeID = UUID.v4(); + var clone = now.clone(); - var startDateOfChallenge = (startDateSaved == null) ? goal.getStartDateOfSession(clone) : startDateSaved; - var endDateOfChallenge = (endDateSaved == null) ? goal.getEndDateOfSession(clone.clone()) : endDateSaved; + var startDateOfChallenge = goal.getStartDateOfSession(clone); + var endDateOfChallenge = goal.getEndDateOfSession(clone.clone()); + + var mapConditionIDToSensorAndTimeBoxRequired:any = {}; + + var goalConditions = goal.getConditions(); + for (var conditionIndex in goalConditions) { + var currentCondition = goalConditions[conditionIndex]; + var conditionID = currentCondition.getID(); + var symbolicNamesAndTimeBoxRequired = currentCondition.getRequiredByCondition(startDateOfChallenge, endDateOfChallenge); + mapConditionIDToSensorAndTimeBoxRequired[conditionID] = symbolicNamesAndTimeBoxRequired; + + } + + var result = this.newChallenge(challengeID, goal, user, mapConditionIDToSensorAndTimeBoxRequired, startDateOfChallenge, endDateOfChallenge, now); + + return result; + } - var newChallenge:UserChallenge = new UserChallenge(goal, user, startDateOfChallenge, endDateOfChallenge); + private newChallenge(challengeID, goal, user, mapConditionIDToSymbolicNamesAndTimeBoxesRequired, startDate, endDate, now):UserChallenge { + var newChallenge:UserChallenge = new UserChallenge(challengeID, goal, user, startDate, endDate, mapConditionIDToSymbolicNamesAndTimeBoxesRequired); - if (newChallenge.getEndDate().isAfter(goal.getEndDate())) { + if (newChallenge.getEndDate().isAfter(goal.getEndOfValidityPeriod())) { return null; } - if (now.isBefore(startDateOfChallenge)) { + if (now.isBefore(startDate)) { newChallenge.setStatus(ChallengeStatus.WAIT); } - if (now.isAfter(startDateOfChallenge) && now.isBefore(endDateOfChallenge)) { + if (now.isAfter(startDate) && now.isBefore(endDate)) { newChallenge.setStatus(ChallengeStatus.RUN); } - return newChallenge; } + + checkDataFromCreate(goal, user, now) { + if (goal == null) { + throw new BadArgumentException('Can not create given challenge because given goal is null'); + } + + if (user == null) { + throw new BadArgumentException('Can not create given challenge because given user is null'); + } + + if (now == null) { + throw new BadArgumentException('Can not create given challenge because given "now" is null'); + } + } + + checkDataFromRestore(data:any, goalRepository:GoalRepository, userRepository:UserRepository):void { + var goalID:string = data.goalID; + if (goalID == null) { + throw new BadArgumentException('Can not restore given challenge because goalID is null'); + } + + var userID:string = data.userID; + if (userID == null) { + throw new BadArgumentException('Can not restore given challenge because userID is null'); + } + + var goal:Goal = goalRepository.getGoal(goalID); + if (goal == null) { + throw new BadArgumentException('Can not restore given challenge because goal with id ' + goalID + ' was not found'); + } + + var user:User = userRepository.getUser(userID); + if (user == null) { + throw new BadArgumentException('Can not restore given challenge because user with id ' + userID + ' was not found'); + } + + var mapConditionIDToSensorAndTimeBoxRequired:any = data.mapConditionIDToSensorAndTimeBoxRequired; + if (mapConditionIDToSensorAndTimeBoxRequired == null) { + throw new BadArgumentException('Can not restore given challenge because map (conditionID) -> to -> (sensor and timeBoxRequired) is null'); + } + } } export = GoalInstanceFactory; \ No newline at end of file diff --git a/backend/src/challenge/UserChallengeRepository.ts b/backend/src/challenge/UserChallengeRepository.ts index e36b960..14e17c8 100644 --- a/backend/src/challenge/UserChallengeRepository.ts +++ b/backend/src/challenge/UserChallengeRepository.ts @@ -65,7 +65,7 @@ class BadgeProvider { var currentBadgeDesc:any = {}; currentBadgeDesc.name = this.goalInstancesArray[i].getName(); currentBadgeDesc.id = this.goalInstancesArray[i].getId(); - currentBadgeDesc.desc = this.goalInstancesArray[i].getDescription(); + currentBadgeDesc.desc = this.goalInstancesArray[i].getGoal().getName(); currentBadgeDesc.progress = this.goalInstancesArray[i].getProgress(); currentBadgeDesc.startDate = this.goalInstancesArray[i].getStartDate(); diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index 730e9b0..234aefe 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -1,5 +1,4 @@ /// -/// /// /// @@ -8,149 +7,102 @@ import Condition = require('./Condition'); import TimeBox = require('../TimeBox'); import Clock = require('../Clock'); import Filter = require('../filter/Filter'); +import ReferencePeriod = require('./ReferencePeriod'); var moment = require('moment'); var moment_timezone = require('moment-timezone'); class AverageOnValue extends Condition { - private oldTimeBox:TimeBox; - private newTimeBox:TimeBox; - private referencePeriod:moment.Moment; - - constructor(id:string, condition:GoalExpression, thresholdRate:number, - startDate:moment.Moment, dateOfCreation:moment.Moment, endDate:moment.Moment, referencePeriod:moment.Moment, - percentageAchieved:number = 0, percentageOfTimeElapsed:number = 0, filter:Filter = null) { - - super(id, condition, thresholdRate, startDate, dateOfCreation, endDate, - percentageAchieved, percentageOfTimeElapsed, filter); - - this.oldTimeBox = new TimeBox(startDate, dateOfCreation); - this.newTimeBox = new TimeBox(dateOfCreation, endDate); + private referencePeriod:ReferencePeriod; + constructor(id:string, description:string, expression:GoalExpression, thresholdRate:number, + filter:Filter, referencePeriod:ReferencePeriod) { + super(id, description, expression, thresholdRate, filter); this.referencePeriod = referencePeriod; } - public setTimeBox(newTimeBox:TimeBox) { - - this.dateOfCreation = newTimeBox.getStartDate(); - this.endDate = newTimeBox.getEndDate(); - - var timeOfTheUltimateOriginOfOrigins:moment.Moment = Clock.getMoment(new Date(0, 0, 0, 0, 0, 0, 0).getTime()); - - var year:number = this.referencePeriod.year() - timeOfTheUltimateOriginOfOrigins.year(); - - var month:number = this.referencePeriod.month() - timeOfTheUltimateOriginOfOrigins.month(); - var day:number = this.referencePeriod.date() - timeOfTheUltimateOriginOfOrigins.date(); + getTimeBoxRequired(startDateOfChallenge:moment.Moment, endDateOfChallenge:moment.Moment):any { - var momentObj:moment.Moment = moment.tz( Clock.getTimeZone()).year(this.dateOfCreation.year() - year).month(this.dateOfCreation.month() - month).date(this.dateOfCreation.date() - day).hours(this.dateOfCreation.hour()).minute(this.dateOfCreation.minute()) - .second(this.dateOfCreation.second()).millisecond(this.dateOfCreation.millisecond()); + var beginningOfReferencePeriod:moment.Moment = this.referencePeriod.getTimeBoxRequired(startDateOfChallenge); + + return { + start: beginningOfReferencePeriod.clone(), + dateOfCreation: startDateOfChallenge.clone(), + end: endDateOfChallenge.clone() + }; + } - this.startDate = momentObj; + public evaluate(data:any, conditionDescription:any):boolean { - var timeBox:TimeBox = new TimeBox(this.startDate, this.endDate); - this.timeBox = timeBox; - } + var remainingData:any = super.keepUsefulValues(data, conditionDescription); + remainingData = super.applyFilters(remainingData); - public evaluate(data:any):boolean { - var remainingData:any = super.applyFilters(data); data = remainingData; - console.log('Remaining data', data); - var sensorNames:string[] = this.expression.getRequired(); - var result = false; - - for (var currentSensorNameIndex in sensorNames) { - - var currentSensorName:string = sensorNames[currentSensorNameIndex]; - - var oldAndNewData:any[] = data[currentSensorName].values; - - var oldData:number[] = []; - var newData:number[] = []; - - this.separateOldAndNewData(oldAndNewData, oldData, newData); + // This type of condition must have one and exactly one required + var currentSensorName:string = sensorNames[0]; - console.log("OLD DATA", oldData, "NEW DATA", newData); + var oldAndNewData:any[] = data[currentSensorName]; - this.percentageAchieved = 0; - var rate = 0; + var timeBox:any = conditionDescription.timeBox; + var dateOfCreation:moment.Moment = timeBox.dateOfCreation; - if (oldData.length != 0 && newData.length != 0) { + var oldData:number[] = []; + var newData:number[] = []; - var result = true; + this.separateOldAndNewData(oldAndNewData, oldData, newData, dateOfCreation); - var oldAverage = this.computeAverageValues(oldData); - var newAverage = this.computeAverageValues(newData); + var percentageAchieved = 0; + var rate = 0; - if (newAverage) { - rate = (newAverage * 100 / oldAverage); - } + if (oldData.length != 0 && newData.length != 0) { - // < baisse - // > hausse - var changeRate = 0; + var oldAverage = this.computeAverageValues(oldData); + var newAverage = this.computeAverageValues(newData); - if (this.expression.getComparisonType() === '<') { - changeRate = 100 - rate; - } else { - changeRate = rate - 100; - } - - this.percentageAchieved = changeRate * 100 / this.thresholdRate; - - // It can be infinite - this.percentageAchieved = (this.percentageAchieved > 100) ? 100 : this.percentageAchieved; - result = result && this.percentageAchieved >= 100; + if (newAverage) { + rate = (newAverage * 100 / oldAverage); } + // < baisse + // > hausse + var changeRate = 0; - this.updateDurationAchieved(Clock.getNow()); - } + if (this.expression.getComparisonType() === '<') { + changeRate = 100 - rate; + } else { + changeRate = rate - 100; + } - return result; - } + percentageAchieved = changeRate * 100 / this.thresholdRate; - public updateDurationAchieved(currentDate:number) { - console.log(this.dateOfCreation.date()); + // It can be infinite + percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; - var currentMoment:moment.Moment = Clock.getMoment(currentDate); - console.log(currentMoment.date()); - if (currentMoment.isBefore(this.dateOfCreation)) { - throw new Error('Time given is before dateOfCreation !'); } + var finished:boolean = percentageAchieved === 100; + var result:any = {percentageAchieved: percentageAchieved, finished: finished}; - var duration = this.endDate.valueOf() - this.dateOfCreation.valueOf(); - var durationAchieved = (currentMoment.valueOf() - this.dateOfCreation.valueOf()); - this.percentageOfTimeElapsed = durationAchieved * 100 / duration; - - // It can have tiny incorrect decimal values - this.percentageOfTimeElapsed = (this.percentageOfTimeElapsed > 100) ? 100 : this.percentageOfTimeElapsed; + return result; } - public separateOldAndNewData(values:any[], oldValues:number[], newValues:number[]) { + public separateOldAndNewData(values:any[], oldValues:number[], newValues:number[], dateOfCreation:moment.Moment) { for (var currentValueIndex in values) { - // { date : __ , value : __ } var currentPairDateValue:any = values[currentValueIndex]; + var currentMoment:moment.Moment = Clock.getMomentFromString(currentPairDateValue.date); - - var currentMoment:moment.Moment = Clock.getMoment(parseInt(currentPairDateValue.date)); - - //console.log("PROCESSING MOMENT", currentMoment.format()); - // console.log("START DATE", this.startDate.format(), "END DATE", this.endDate.format()); - - if (currentMoment.isAfter(this.startDate) - && currentMoment.isBefore(this.dateOfCreation)) { - oldValues.push(currentPairDateValue.value); - } - else if (currentMoment.isAfter(this.dateOfCreation)) { + if (currentMoment.isAfter(dateOfCreation)) { newValues.push(currentPairDateValue.value); } + else if (currentMoment.isBefore(dateOfCreation)) { + oldValues.push(currentPairDateValue.value); + } } } @@ -170,7 +122,6 @@ class AverageOnValue extends Condition { public getDataInJSON():any { var data:any = super.getDataInJSON(); data.type = 'comparison'; - data.expression.periodOfTime = (''+this.referencePeriod.valueOf()); return data; } diff --git a/backend/src/condition/Condition.ts b/backend/src/condition/Condition.ts index baffb69..cd52421 100644 --- a/backend/src/condition/Condition.ts +++ b/backend/src/condition/Condition.ts @@ -14,189 +14,133 @@ import Clock = require('../Clock'); class Condition { protected id:string; + protected description:string; protected expression:GoalExpression; protected thresholdRate:number; - protected startDate:moment.Moment; - protected dateOfCreation:moment.Moment; - protected endDate:moment.Moment; - - protected timeBox:TimeBox; - - protected percentageAchieved:number; - protected percentageOfTimeElapsed:number; - protected filter:Filter; - /** - * Constructor of base class Condition, it allows you to build a goal condition - * @param id - * The id of the condition. If null, the id will be generated. - * @param expression - * The expression object, for instance 'TMP_Cli > 25' - * @param thresholdRate - * This value represents the threshold rate when the condition must be verified - * @param startDate - * The start of the application of the current condition - for instance start date of the goal instance - * @param dateOfCreation - * The date of creation of the parent goal - * @param endDate - * The end of the application of the current condition - for instance end date of the goal instance - * @param percentageAchieved - * The percentage of progression achieved - * @param percentageOfTimeElapsed - * The percentage of time elapsed between 'now' and startDate - */ - constructor(id:string, expression:GoalExpression, thresholdRate:number, - startDate:moment.Moment, dateOfCreation:moment.Moment, endDate:moment.Moment, - percentageAchieved:number = 0, percentageOfTimeElapsed:number = 0, filter:Filter = null) { + constructor(id:string, description:string, expression:GoalExpression, thresholdRate:number, filter:Filter = null) { + + this.id = id; - this.id = (id) ? id : UUID.v4(); + this.description = description; this.expression = expression; this.thresholdRate = thresholdRate; - this.startDate = startDate; - this.dateOfCreation = dateOfCreation; - this.endDate = endDate; - - this.timeBox = new TimeBox(this.startDate, this.endDate); - - this.percentageAchieved = percentageAchieved; - this.percentageOfTimeElapsed = percentageOfTimeElapsed; - this.filter = (filter) ? filter : new Filter('all', ['all']); } - getStringRepresentation():string { - return this.expression.getStringRepresentation() + " - " + this.expression.getDescription() + " filtre " + JSON.stringify(this.filter.getDataInJSON()); - } - /** - * This method will return the id of the current condition - * @returns {string} - * The id of the current condition - */ - getID():string { - return this.id; + getTimeBoxRequired(startDateOfChallenge:moment.Moment, endDateOfChallenge:moment.Moment):any { + + // Must be overridden by any condition that need more time than + // start date and end date of challenge (caller) + return {start: startDateOfChallenge, end: endDateOfChallenge}; } /** - * This method will return the field required by its expression, - * the symbolic name(s) of the expression. - * See GoalExpression#getRequired method - * @returns {string[]} - * The array of symbolic names in the expression + * + * @param data + * { + * : [ {date : ., value : .}, ... ] + * } + * @param conditionDescription + * { + * timeBox: { + * start: ., + * end: . + * } + * } + * @returns {any[]} */ - getRequired():any { + keepUsefulValues(data:any, conditionDescription:any):any { var result:any = {}; - var sensorRequired:string[] = this.expression.getRequired(); - - for (var currentSensorRequiredIndex in sensorRequired) { - var currentSensorRequired:string = sensorRequired[currentSensorRequiredIndex]; - result[currentSensorRequired] = this.timeBox.getRequired(); - } - - return result; - } - hasLeftOperand(operandName:string):boolean { - return this.expression.hasLeftOperand(operandName); - } - - hasRightOperand(operandName:string):boolean { - return this.expression.hasRightOperand(operandName); - } + var startDate:moment.Moment = conditionDescription.timeBox.start; + var endDate:moment.Moment = conditionDescription.timeBox.end; - hasComparisonType(comparisonType:string):boolean { - return this.expression.getComparisonType() === comparisonType; - } + // For each symbolic names in data + for (var currentSymbolicName in data) { + var currentResult:any[] =[]; - getStartDate():moment.Moment { - return this.startDate; - } + var currentDataArray:any = data[currentSymbolicName]; + for (var currentDataIndex in currentDataArray) { + var currentData:any = currentDataArray[currentDataIndex]; + var date:moment.Moment = Clock.getMomentFromString(currentData.date); + if (date.isAfter(startDate) && date.isBefore(endDate)) { + currentResult.push(currentData); + } + } - setStartDate(newStartDate:moment.Moment):void { - this.startDate = newStartDate; - } + result[currentSymbolicName] = currentResult; + } - getEndDate():moment.Moment { - return this.endDate; + return result; } - setEndDate(newEndDate:moment.Moment):void { - this.endDate = newEndDate; - } + getRequiredByCondition(startDate, endDate) { + var result:any = {}; + var symbolicNames:string[] = this.expression.getRequired(); - setTimeBox(newTimeBox:TimeBox) { + result.symbolicNames = symbolicNames; + result.timeBox = this.getTimeBoxRequired(startDate, endDate); - this.timeBox = newTimeBox; - this.startDate = newTimeBox.getStartDate(); - this.endDate = newTimeBox.getEndDate(); - console.log("TIMEBOX SET AT", newTimeBox, "So now, condition have", this.startDate.format(), "and", this.endDate.format()); + return result; } - isInTimeBox(date:moment.Moment):boolean { - return this.timeBox.isDateInTimeBox(date); + getStringRepresentation():string { + return this.expression.getStringRepresentation() + " - filtre " + JSON.stringify(this.filter.getDataInJSON()); } - setPercentageAchieved(newPercentageAchieved:number) { - this.percentageAchieved = newPercentageAchieved; + getID():string { + return this.id; } - updatePercentageOfTimeElapsed(currentDate:number) { - - var currentMoment:moment.Moment = Clock.getMoment(currentDate); - if (currentMoment.isBefore(this.getStartDate())) { - throw new Error('Time given is before dateOfCreation !'); - } - - var duration = this.getEndDate().valueOf() - this.getStartDate().valueOf(); - - var durationAchieved = currentMoment.valueOf() - this.getStartDate().valueOf(); - this.percentageOfTimeElapsed = durationAchieved * 100 / duration; - } - getPercentageOfTimeElapsed():number { - return this.percentageOfTimeElapsed; + hasLeftOperand(operandName:string):boolean { + return this.expression.hasLeftOperand(operandName); } - setPercentageOfTimeElapsed(newPercentageOfTimeElapsed:number) { - this.percentageOfTimeElapsed = newPercentageOfTimeElapsed; + hasRightOperand(operandName:string):boolean { + return this.expression.hasRightOperand(operandName); } - getPercentageAchieved():number { - return this.percentageAchieved; + hasComparisonType(comparisonType:string):boolean { + return this.expression.getComparisonType() === comparisonType; } getDataInJSON():any { return { id: this.id, + description: this.description, expression: this.expression.getDataInJSON(), threshold: this.thresholdRate, - startDate: this.startDate, - dateOfCreation: this.dateOfCreation, - endDate: this.endDate, - percentageAchieved: this.percentageAchieved, - percentageOfTimeElapsed: this.percentageOfTimeElapsed, filter: this.filter.getDataInJSON() } } - evaluate(data:any):boolean { + /** + * + * @param data + * [ { date : ..., value : ...} ] + * @param conditionDescription + * { + * symbolic_names: [..], + * timeBox: { + * start:..., + * end:... + * } + * } + */ + evaluate(data:any, conditionDescription:any):any { throw new Error('Can not call base class method ! Must be overridden and implemented.'); } applyFilters(data:any):any { var remainingData = this.filter.apply(data); - /* - console.log("APPLICATION DU FILTRE SUR", JSON.stringify(data)); - console.log("FILTER?", this.filter.getDataInJSON()); - - - console.log("REMAINING DATA AFTER FITLER", JSON.stringify(remainingData)); - */ return remainingData; } } diff --git a/backend/src/condition/ConditionList.ts b/backend/src/condition/ConditionList.ts deleted file mode 100644 index 3fbe930..0000000 --- a/backend/src/condition/ConditionList.ts +++ /dev/null @@ -1,154 +0,0 @@ -/// - -var merge:any = require('merge'); -import uuid = require('node-uuid'); - -import Condition = require('./Condition'); -import GoalExpression = require('./expression/GoalExpression'); -import TimeBox = require('../TimeBox'); - -import Challenge = require('../challenge/UserChallenge'); - -class ConditionList { - - private conditions:Condition[] = []; - - public getConditions():Condition[] { - return this.conditions; - } - - public addCondition(condition:Condition):void { - this.conditions.push(condition); - } - - public setTimeBoxes(newTimeBox:TimeBox) { - - for (var currentExpressionIndex in this.conditions) { - this.conditions[currentExpressionIndex].setTimeBox(newTimeBox); - } - } - - /** - * - * @returns {any} - * { - * : { start:_, end:_ } <-- timebox for the sensor-name - * ... - * } - */ - public getRequired():any { - - var result:any = {}; - - // For each expression - for (var currentConditionIndex in this.conditions) { - - // Get its description --> { : null | {start:_, end:_} } - var currentCondition:Condition = this.conditions[currentConditionIndex]; - var currentConditionDesc = currentCondition.getRequired(); - - // for each sensors required by the current expression - for (var currentSensorName in currentConditionDesc) { - - // check if current sensor has already been added - if (result[currentSensorName] != null) { - // if so, retrieve previous timeBox and current timeBox - var currentTimeBox = currentConditionDesc[currentSensorName]; - var oldTimeBox = result[currentSensorName]; - - // merge two timeBoxes - var newTimeBox = this.mergeTimeBox(currentTimeBox, oldTimeBox); - - // add newly merged timeBox into result - result[currentSensorName] = newTimeBox; - } - else { - // if not, add current expression description - result[currentSensorName] = currentConditionDesc[currentSensorName]; - } - } - } - - return result; - } - - // TODO test - public mergeTimeBox(currentTimeBox:any, oldTimeBox:any):any { - if (currentTimeBox == null || oldTimeBox == null) { - return null; - } - - var currentStart = currentTimeBox.start; - var currentEnd = currentTimeBox.end; - - var oldStart = oldTimeBox.start; - var oldEnd = oldTimeBox.end; - - var newStart = (currentStart > oldStart) ? oldStart : currentStart; - var newEnd = (currentEnd > oldEnd) ? currentEnd : oldEnd; - - /* - console.log("currentStart", currentStart, "currentEnd", currentEnd, "oldStart", oldStart, - "oldEnd", oldEnd, "newstart", newStart, "newEnd", newEnd); - */ - - return { - start: newStart, - end: newEnd - } - } - - /** - * - * @param values - * { - * : { timebox { start:_, end:_ }, values : [ {date:_, value:_}, ... ] } - * ... - * } - * @returns {boolean} - */ - - public evaluate(values:any, challenge:Challenge):boolean { - - var result:boolean = true; - - for (var i = 0; i < this.conditions.length; i++) { - - result = result && this.conditions[i].evaluate(values); - - var conditionDescription:any = this.conditions[i].getDataInJSON(); - if (challenge != null) { - - // TODO delete following call - challenge.addProgress(conditionDescription); - challenge.addProgressByCondition(this.conditions[i].getID(), this.conditions[i].getPercentageAchieved()); - } - } - - return result; - } - - public getDataInJSON():any { - var result:any[] = []; - - for (var i = 0; i < this.conditions.length; i++) { - result.push(this.conditions[i].getDataInJSON()); - } - - return result; - } - - getStringRepresentation():string { - var result:string = ''; - - for(var currentConditionIndex in this.conditions) { - var currentCondition = this.conditions[currentConditionIndex]; - result += '\t|\t\t' + currentCondition.getStringRepresentation(); - } - - return result; - } - -} - -export = ConditionList; diff --git a/backend/src/condition/OverallGoalCondition.ts b/backend/src/condition/OverallGoalCondition.ts index 7ed0474..c28f757 100644 --- a/backend/src/condition/OverallGoalCondition.ts +++ b/backend/src/condition/OverallGoalCondition.ts @@ -14,78 +14,56 @@ import BadArgumentException = require('../exceptions/BadArgumentException'); class OverallGoalCondition extends Condition { - - constructor(id:string, condition:GoalExpression, thresholdRate:number, - startDate:moment.Moment, dateOfCreation:moment.Moment, endDate:moment.Moment, - percentageAchieved:number = 0, percentageOfTimeElapsed:number = 0, filter:Filter = null) { - - super(id, condition, thresholdRate, startDate, dateOfCreation, endDate, - percentageAchieved, percentageOfTimeElapsed, filter); + constructor(id:string, description:string, condition:GoalExpression, thresholdRate:number, filter:Filter) { + super(id, description, condition, thresholdRate, filter); } - /** - * - * @param values - * { - * : { timebox { start:_, end:_ }, values : [ {date:_, value:_}, ... ] } - * ... - * } - */ + public evaluate(data:any, conditionDescription:any) { - public evaluate(data:any) { + var remainingData:any = super.keepUsefulValues(data, conditionDescription); + + remainingData = super.applyFilters(remainingData); - var remainingData:any = super.applyFilters(data); data = remainingData; var conditionDesc:string[] = this.expression.getRequired(); // For each sensors required by internal condition - for (var currentSensorNameIndex in conditionDesc) { - var currentSensorName:string = conditionDesc[currentSensorNameIndex]; + for (var currentSymbolicNameIndex in conditionDesc) { + var currentSymbolicName:string = conditionDesc[currentSymbolicNameIndex]; // Retrieve values associated - var currentConditionDesc = data[currentSensorName]; + var values:any[] = data[currentSymbolicName]; - if(!currentConditionDesc) { - throw new BadArgumentException('Can not evaluate condition ! Proper argument were not provided. Field' + currentSensorName + ' is missing'); + if (values == null) { + throw new BadArgumentException('Can not evaluate condition ! Proper argument were not provided. Field' + currentSymbolicName + ' is missing'); } - - var values:any[] = currentConditionDesc.values; - var numberOfValues:number = (values).length; var numberOfCorrectValues:number = 0; // Check how many values are correct - for (var currentValueIndex in values) { - var value = values[currentValueIndex]; - - var date:moment.Moment = Clock.getMomentFromString(value.date); + for (var currentPairDateValueIndex in values) { + var currentPairDateValue = values[currentPairDateValueIndex]; - if (this.isInTimeBox(date)) { - var dataToEvaluate:any = {}; - dataToEvaluate[currentSensorName] = value.value; + var dataToEvaluate:any = {}; + dataToEvaluate[currentSymbolicName] = currentPairDateValue.value; - // Check value by value if internal condition is satisfied - if (this.expression.evaluate(dataToEvaluate)) { - ++numberOfCorrectValues; - } - } - else { - numberOfValues--; + // Check value by value if internal condition is satisfied + if (this.expression.evaluate(dataToEvaluate)) { + ++numberOfCorrectValues; } } } - this.percentageAchieved = ((numberOfCorrectValues * 100 / numberOfValues) * 100) / this.thresholdRate; - - this.percentageAchieved = (this.percentageAchieved > 100) ? 100 : this.percentageAchieved; + var percentageAchieved = ((numberOfCorrectValues * 100 / numberOfValues) * 100) / this.thresholdRate; + percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; - this.updatePercentageOfTimeElapsed(Clock.getNow()); + var finished:boolean = percentageAchieved === 100; - return (numberOfCorrectValues * 100 / numberOfValues) >= this.thresholdRate; + return {percentageAchieved: percentageAchieved, finished: finished}; } public getDataInJSON():any { diff --git a/backend/src/condition/ReferencePeriod.ts b/backend/src/condition/ReferencePeriod.ts new file mode 100644 index 0000000..6f1f5e1 --- /dev/null +++ b/backend/src/condition/ReferencePeriod.ts @@ -0,0 +1,25 @@ +/// +/// +/// + +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + +class ReferencePeriod { + + private numberOfUnitToSubtract:number; + private unitToSubtract:string; + + constructor(numberOfUnitToSubtract:number, unitToSubtract:string) { + this.numberOfUnitToSubtract = numberOfUnitToSubtract; + this.unitToSubtract = unitToSubtract; + } + + getTimeBoxRequired(startDate:moment.Moment) { + var dateOfCreation:moment.Moment = startDate.clone(); + dateOfCreation = dateOfCreation.subtract(this.numberOfUnitToSubtract, this.unitToSubtract); + return dateOfCreation; + } +} + +export = ReferencePeriod; \ No newline at end of file diff --git a/backend/src/condition/expression/GoalExpression.ts b/backend/src/condition/expression/GoalExpression.ts index 2215b64..cbb7307 100644 --- a/backend/src/condition/expression/GoalExpression.ts +++ b/backend/src/condition/expression/GoalExpression.ts @@ -5,15 +5,12 @@ import Comparator = require('./Comparator'); import BadArgumentException = require('../../exceptions/BadArgumentException'); class GoalExpression { - private description:string; - private leftOperand:Operand; private rightOperand:Operand; private comparator:Comparator; - constructor(leftOperand:Operand, typeOfComparison:string, rightOperand:Operand, description:string) { - this.description = description; + constructor(leftOperand:Operand, typeOfComparison:string, rightOperand:Operand) { this.leftOperand = leftOperand; this.rightOperand = rightOperand; @@ -21,10 +18,6 @@ class GoalExpression { this.comparator = new Comparator(typeOfComparison); } - public getDescription():string { - return this.description; - } - public getComparisonType():string { return this.comparator.getTypeOfComparison(); } @@ -126,8 +119,7 @@ class GoalExpression { value: this.rightOperand.getStringDescription(), sensor: this.rightOperand.hasToBeDefined() }, - comparison: this.comparator.getTypeOfComparison(), - description: this.description, + comparison: this.comparator.getTypeOfComparison() }; } } diff --git a/backend/src/condition/factory/ConditionFactory.ts b/backend/src/condition/factory/ConditionFactory.ts index 5e62089..9c7e061 100644 --- a/backend/src/condition/factory/ConditionFactory.ts +++ b/backend/src/condition/factory/ConditionFactory.ts @@ -12,16 +12,17 @@ import AverageOnValue = require('../AverageOnValue'); import ExpressionFactory = require('./ExpressionFactory'); import Clock = require('../../Clock'); import Filter = require('../../filter/Filter'); +import ReferencePeriod = require('../ReferencePeriod'); class ConditionFactory { private expressionFactory:ExpressionFactory = new ExpressionFactory(); - public createCondition(data:any, goalTimeBox:any, duration:number):Condition { + public createCondition(data:any, goalTimeBox:any):Condition { var type:string = data.type; var expression = null; switch (type) { case 'overall': - expression = this.createOverall(data, goalTimeBox, duration); + expression = this.createOverall(data, goalTimeBox); break; case 'comparison': expression = this.createComparison(data); @@ -33,11 +34,11 @@ class ConditionFactory { return expression; } - public createOverall(data:any, goaltimeBox:any, duration:number):Condition { + public createOverall(data:any, goaltimeBox:any):Condition { data.expression.timeBox = goaltimeBox; - var goalCondition:GoalExpression = this.expressionFactory.createExpression(data.expression); + var goalExpression:GoalExpression = this.expressionFactory.createExpression(data.expression); var startDateOfValidityPeriod:moment.Moment = goaltimeBox.startDate; var endDateOfValidityPeriod:moment.Moment = goaltimeBox.endDate; @@ -48,8 +49,7 @@ class ConditionFactory { var periodOfDayFilterDesc:string[] = data.filter.periodOfDayFilter; var filter:Filter = new Filter(dayOfWeekFilterDesc, periodOfDayFilterDesc); - - var overallCondition:OverallGoalCondition = new OverallGoalCondition(null, goalCondition, threshold, startDateOfValidityPeriod, Clock.getMoment(Clock.getNow()), endDateOfValidityPeriod, 0, 0, filter); + var overallCondition:OverallGoalCondition = new OverallGoalCondition(null, '', goalExpression, threshold, filter); return overallCondition; } @@ -58,8 +58,11 @@ class ConditionFactory { var periodOfDayFilterDesc:string[] = data.filter.periodOfDayFilter; var filter:Filter = new Filter(dayOfWeekFilterDesc, periodOfDayFilterDesc); + var referencePeriodDesc = data.referencePeriod; + var referencePeriod:ReferencePeriod = new ReferencePeriod(referencePeriodDesc.numberOfUnitToSubtract, referencePeriodDesc.unitToSubtract); + var goalExpression:GoalExpression = this.expressionFactory.createExpression(data.expression); - var averageOnValue:AverageOnValue = new AverageOnValue(null, goalExpression, data.threshold, data.startDate, data.dateOfCreation, data.endDate,Clock.getMoment(new Date(parseInt(data.expression.periodOfTime)).getTime()),0,0,filter); + var averageOnValue:AverageOnValue = new AverageOnValue(null, '', goalExpression, data.threshold, filter, referencePeriod); return averageOnValue; } diff --git a/backend/src/condition/factory/ExpressionFactory.ts b/backend/src/condition/factory/ExpressionFactory.ts index b17a7d5..130c079 100644 --- a/backend/src/condition/factory/ExpressionFactory.ts +++ b/backend/src/condition/factory/ExpressionFactory.ts @@ -4,41 +4,56 @@ import Operand = require('../expression/Operand'); import TimeBox = require('../../TimeBox'); +import BadArgumentException = require('../../exceptions/BadArgumentException'); class ExpressionFactory { - static REQUIRED_JSON_FIELD:string[] = ['comparison', 'valueLeft', 'valueRight', 'description']; + public createExpression(data:any):GoalExpression { + this.checksData(data); - public createExpression(expression:any):GoalExpression { - for (var currentRequiredFieldIndex in ExpressionFactory.REQUIRED_JSON_FIELD) { - var currentRequiredField = ExpressionFactory.REQUIRED_JSON_FIELD[currentRequiredFieldIndex]; + var leftOperandName = data.valueLeft.value; + var leftOperandRequired = data.valueLeft.symbolicName; + var leftOperand:Operand = new Operand(leftOperandName, leftOperandRequired); + + var rightOperandName = data.valueRight.value; + var rightOperandRequired = data.valueRight.symbolicName; + var rightOperand:Operand = new Operand(rightOperandName, rightOperandRequired); + + var typeOfComparison:string = data.comparison; - if (!expression[currentRequiredField] || expression[currentRequiredField] === "undefined") { - throw new Error('Can not build expression ! Field ' - + currentRequiredField + ' is missing'); - } + var newExpression:GoalExpression = new GoalExpression(leftOperand, typeOfComparison, rightOperand); + return newExpression; + } + + private checksData(data:any) { + if(data.valueLeft == null) { + throw new BadArgumentException('Can not build expression, field "valueLeft" is null'); } - var leftOperandName = expression.valueLeft.value; - var leftOperandRequired = expression.valueLeft.sensor; - var leftOperand:Operand = new Operand(leftOperandName, leftOperandRequired); + if(data.valueLeft.value == null) { + throw new BadArgumentException('Can not build expression, field "valueLeft.value" is null'); + } - var rightOperandName = expression.valueRight.value; - var rightOperandRequired = expression.valueRight.sensor; - var rightOperand:Operand = new Operand(rightOperandName, rightOperandRequired); + if(data.valueLeft.symbolicName == null) { + throw new BadArgumentException('Can not build expression, field "valueLeft.symbolicName" is null'); + } + + if(data.valueRight == null) { + throw new BadArgumentException('Can not build expression, field "valueRight" is null'); + } - var typeOfComparison:string = expression.comparison; - var description:string = expression.description; + if(data.valueRight.value == null) { + throw new BadArgumentException('Can not build expression, field "valueRight.value" is null'); + } + + if(data.valueRight.symbolicName == null) { + throw new BadArgumentException('Can not build expression, field "valueRight.symbolicName" is null'); + } - /*FIXME - var timeBox:any = expression.timeBox; - var startDate:number = timeBox.startDate; - var endDate:number = timeBox.endDate; + if(data.comparison == null) { + throw new BadArgumentException('Can not build expression, field "comparison" is null'); + } - var timeBoxObj:TimeBox = new TimeBox(startDate, endDate); -*/ - var newGoalCondition:GoalExpression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - return newGoalCondition; } } diff --git a/backend/src/filter/Filter.ts b/backend/src/filter/Filter.ts index 48c6213..5fe35d6 100644 --- a/backend/src/filter/Filter.ts +++ b/backend/src/filter/Filter.ts @@ -34,7 +34,7 @@ class Filter { for (var currentSensorName in data) { var correctValues:any[] = []; - var arrayOfValues = data[currentSensorName].values; + var arrayOfValues = data[currentSensorName]; //console.log("Array of values", arrayOfValues); @@ -80,9 +80,7 @@ class Filter { } - var correctValuesContainer:any = {}; - correctValuesContainer.values = correctValues; - result[currentSensorName] = correctValuesContainer; + result[currentSensorName] = correctValues; } //console.log("DONC ON GARDE", result); diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index 3864241..96c15ba 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -9,7 +9,6 @@ var moment_timezone = require('moment-timezone'); import uuid = require('node-uuid'); -import ConditionList = require('../condition/ConditionList'); import Condition = require('../condition/Condition'); import Challenge = require('../challenge/UserChallenge'); import TimeBox = require('../TimeBox'); @@ -20,39 +19,35 @@ import RecurringSession = require('./RecurringSession'); class Goal { private id; private name:string; - private conditionsList:ConditionList; - - private startDate:moment.Moment; - private endDate:moment.Moment; - - private durationInDays:number; - private recurringSession:RecurringSession; - private badgeID:string; + private conditionsArray:Condition[] = []; - constructor(name:string, startDate:moment.Moment, endDate:moment.Moment, durationInDays:number, badgeID:string, id = null, recurringSession:RecurringSession = new RecurringSession('month')) { - if (!name) { - throw new Error('Bad argument : name given is null'); - } + private beginningOfValidityPeriod:moment.Moment; + private endOfValidityPeriod:moment.Moment; - this.conditionsList = new ConditionList(); + private recurringSession:RecurringSession; - this.badgeID = badgeID; + constructor(id:string, name:string, badgeID:string, beginningOfValidityPeriod:moment.Moment, + endOfValidityPeriod:moment.Moment, recurringSession:RecurringSession) { + this.id = id; this.name = name; + this.badgeID = badgeID; + - this.id = (id) ? id : uuid.v4(); + this.beginningOfValidityPeriod = beginningOfValidityPeriod; + this.endOfValidityPeriod = endOfValidityPeriod; - if (startDate != null && endDate != null && endDate.isBefore(startDate)) { - throw new Error('End date is before start date'); - } + this.recurringSession = recurringSession; + } - this.startDate = startDate; - this.endDate = endDate; - this.recurringSession = recurringSession; + getBeginningOfValidityPeriod():moment.Moment { + return this.beginningOfValidityPeriod; + } - this.durationInDays = durationInDays; + getEndOfValidityPeriod():moment.Moment { + return this.endOfValidityPeriod; } getStartDateOfSession(now) { @@ -67,70 +62,62 @@ class Goal { return this.badgeID; } - public getUUID() { + getUUID() { return this.id; } - public hasUUID(aUUID:string):boolean { + hasUUID(aUUID:string):boolean { return this.id === aUUID; } - public setUUID(aUUID) { + setUUID(aUUID) { this.id = aUUID; } - public setTimeBoxes(newTimeBox:TimeBox) { - this.conditionsList.setTimeBoxes(newTimeBox); - } - public getStartDate():moment.Moment { - return this.startDate; + getName():string { + return this.name; } - public getEndDate():moment.Moment { - return this.endDate; + public addCondition(condition:Condition) { + this.conditionsArray.push(condition); } - public getDuration():number { - return this.durationInDays; - } + public evaluate(values:any, challenge:Challenge):boolean { - public getName():string { - return this.name; - } + challenge.resetProgress(); - public addCondition(expression:Condition) { - this.conditionsList.addCondition(expression); - } + var result:boolean = true; + for (var i = 0; i < this.conditionsArray.length; i++) { + var currentCondition:Condition = this.conditionsArray[i]; + + var currentConditionDescription:any = challenge.getConditionDescriptionByID(currentCondition.getID()); + var currentConditionState = currentCondition.evaluate(values,currentConditionDescription); - public evaluate(values:any, challenge:Challenge = null):boolean { + result = result && currentConditionState.finished; - if (challenge != null) { - challenge.resetProgress(); + challenge.addProgressByCondition(currentCondition.getID(), currentConditionState.percentageAchieved); } - return this.conditionsList.evaluate(values, challenge); + return result; } - public getRequired():any { - return this.conditionsList.getRequired(); - } + public getRequired(startDateOfChallenge, endDateOfChallenge):any { + + var result:any = {}; + for(var conditionIndex in this.conditionsArray) { + var currentCondition = this.conditionsArray[conditionIndex]; + var currentConditionID = currentCondition.getID(); + var currentConditionRequired = currentCondition.getRequiredByCondition(startDateOfChallenge, endDateOfChallenge); + result[currentConditionID] = currentConditionRequired; + } - public getConditions():ConditionList { - return this.conditionsList; + + return result; } - public getData():any { - return { - "name": this.name, - "conditions": this.conditionsList.getDataInJSON(), - "timeBox": { - "startDate": this.startDate, - "endDate": this.endDate - }, - "durationInDays": this.durationInDays, - "badge": this.badgeID - } + public getConditions():Condition[] { + return this.conditionsArray; } public getDataInJSON():any { @@ -138,18 +125,41 @@ class Goal { id: this.id, name: this.name, timeBox: { - startDate: this.startDate, - endDate: this.endDate + startDate: this.beginningOfValidityPeriod, + endDate: this.endOfValidityPeriod }, duration: this.recurringSession.getDescription(), - conditions: this.conditionsList.getDataInJSON(), + conditions: this.getDataOfConditionsInJSON(), badgeID: this.badgeID } } + public getDataOfConditionsInJSON():any { + var result:any[] = []; + + for (var i = 0; i < this.conditionsArray.length; i++) { + result.push(this.conditionsArray[i].getDataInJSON()); + } + + return result; + } + + getStringRepresentation():string { - return '\n#' + this.id + '\t' + this.name + '\n-\t' + this.startDate.toISOString() + ' :: ' + this.endDate.toISOString() + '\n' + - ' - Récurrent : ' + this.recurringSession.getDescription() + '\n' + this.conditionsList.getStringRepresentation(); + return '\n#' + this.id + '\t' + this.name + '\n-\t' + this.beginningOfValidityPeriod.toISOString() + ' :: ' + this.endOfValidityPeriod.toISOString() + '\n' + + ' - Récurrent : ' + this.recurringSession.getDescription() + '\n' + this.getStringRepresentationOfCondition(); + } + + + getStringRepresentationOfCondition():string { + var result:string = ''; + + for(var currentConditionIndex in this.conditionsArray) { + var currentCondition = this.conditionsArray[currentConditionIndex]; + result += '\t|\t\t' + currentCondition.getStringRepresentation(); + } + + return result; } } diff --git a/backend/src/goal/GoalFactory.ts b/backend/src/goal/GoalFactory.ts index 15397df..8ddf912 100644 --- a/backend/src/goal/GoalFactory.ts +++ b/backend/src/goal/GoalFactory.ts @@ -4,6 +4,7 @@ var moment = require('moment'); var moment_timezone = require('moment-timezone'); +import UUID = require('node-uuid'); import Goal = require('./Goal'); import ConditionFactory = require('../condition/factory/ConditionFactory'); @@ -19,47 +20,73 @@ class GoalFactory { this.conditionFactory = new ConditionFactory(); } - public createGoal(data:any):Goal { - var goalName:string = data.name; - - var startDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.timeBox.startDate); - var endDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.timeBox.endDate); - - var durationAllowedDesc:string = data.duration; - var durationAllowed:number = 0; - - switch (durationAllowedDesc) { - case 'day': - durationAllowed = 1; - break; - case 'week' : - durationAllowed = 7; - break; - case 'month' : - durationAllowed = 30; - break; - default : - throw new BadArgumentException('Can not build goal. Given duration allowed' + durationAllowedDesc + ' is unknown'); + public restoreGoal(data:any):Goal { + if (data.id == null) { + throw new BadArgumentException('Can not create given goal because field "id" is null'); } + return this.createGoal(data); + } + public createGoal(data:any):Goal { + var goalID = (data.id == null) ? UUID.v4() : data.id; + var goalName:string = data.name; var badge:string = data.badgeID; - var goalID:string = data.id; - var recurringType = new RecurringSession(durationAllowedDesc); + var startDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.validityPeriod.start); + var endDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.validityPeriod.end); + + var recurringType:string = data.recurringPeriod; + var recurringPeriod = new RecurringSession(recurringType); - var newGoal:Goal = new Goal(goalName, startDateOfValidityPeriod, endDateOfValidityPeriod, - durationAllowed, badge, goalID,recurringType); + var newGoal:Goal = new Goal(goalID, goalName, badge, startDateOfValidityPeriod, endDateOfValidityPeriod, recurringPeriod); var goalConditions:any[] = data.conditions; for (var i = 0; i < goalConditions.length; i++) { - var currentExpression = this.conditionFactory.createCondition(goalConditions[i], data.timeBox, durationAllowed); + var currentExpression = this.conditionFactory.createCondition(goalConditions[i], data.timeBox); newGoal.addCondition(currentExpression); } // console.log("Creation de l'objectif", goalName, "valide du", startDateOfValidityPeriod, "au", endDateOfValidityPeriod, "avec le badge", newGoal.getBadgeID()); return newGoal; } + + private checkData(data:any) { + if (data.name == null) { + throw new BadArgumentException('Can not create given goal because field "name" is null'); + } + + if (data.badgeID == null) { + throw new BadArgumentException('Can not create given goal because field "badgeID" is null'); + } + + if (data.conditions == null) { + throw new BadArgumentException('Can not create given goal because array "conditions" is null'); + } + + if (data.recurringPeriod == null) { + throw new BadArgumentException('Can not create given goal because array "recurringPeriod" is null'); + } + + if (data.validityPeriod == null) { + throw new BadArgumentException('Can not create given goal because field "validityPeriod" is missing'); + } + + if (data.validityPeriod.start == null) { + throw new BadArgumentException('Can not create given goal because field "validityPeriod.start" is null'); + } + + if (data.validityPeriod.end == null) { + throw new BadArgumentException('Can not create given goal because field "validityPeriod.end" is null'); + } + + var startDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.validityPeriod.start); + var endDateOfValidityPeriod:moment.Moment = Clock.getMomentFromString(data.validityPeriod.end); + + if (startDateOfValidityPeriod != null && endDateOfValidityPeriod != null && endDateOfValidityPeriod.isBefore(startDateOfValidityPeriod)) { + throw new BadArgumentException('Can not create given goal because "validityPeriod.end" is before "validityPeriod.start"'); + } + } } export = GoalFactory; \ No newline at end of file diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index 779a6bc..df4a36a 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -70,7 +70,7 @@ class GoalDefinitionRepository { for (var currentChallengeIDIndex in currentChallengesID) { var currentChallengeID:string = currentChallengesID[currentChallengeIDIndex]; var currentChallenge:Challenge = challengeRepository.getGoalInstance(currentChallengeID); - takenGoals.push(currentChallenge.getGoalDefinition()); + takenGoals.push(currentChallenge.getGoal()); } var goals:Goal[] = this.diffBetweenTakenGoalsAndAvailableGoals(takenGoals, this.goals); @@ -93,7 +93,7 @@ class GoalDefinitionRepository { for (var currentAvailableGoalIndex in availableGoals) { var currentAvailableGoal = availableGoals[currentAvailableGoalIndex]; - if(!this.goalExistsIn(currentAvailableGoal.getUUID(), takenGoals)) { + if (!this.goalExistsIn(currentAvailableGoal.getUUID(), takenGoals)) { result.push(currentAvailableGoal); } } @@ -127,19 +127,6 @@ class GoalDefinitionRepository { return result; } - public evaluateGoal(data:any):boolean { - var goalID:string = data.id; - var goal:Goal = this.getGoal(goalID); - - var goalValues:any[] = data.values; - var values = []; - for (var i = 0; i < goalValues.length; i++) { - values.push(goalValues[i].value); - } - - return goal.evaluate(values); - } - public getDataInJSON():any { var result:any[] = []; diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts index 6c7df78..ab7483c 100644 --- a/backend/src/user/Entity.ts +++ b/backend/src/user/Entity.ts @@ -12,20 +12,18 @@ class Entity { private id; private name:string; private currentChallenges:string[] = []; - private finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; + private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; - constructor(name:string, id = null, currentChallenges:string[] = [], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}) { + constructor(id:string, name:string, currentChallenges:string[], + finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap) { - this.id = (id) ? id : uuid.v4(); + this.id = id; this.name = name; this.currentChallenges = currentChallenges; - this.finishedBadgesMap = finishedBadgesMap; + this.badgesMap = finishedBadgesMap; } - getCurrentChallenges():string [] { - return this.currentChallenges; - } getUUID() { return this.id; @@ -51,7 +49,11 @@ class Entity { this.name = name; } - public addChallengeFromGoal(goal:Goal):Challenge { + getCurrentChallenges():string [] { + return this.currentChallenges; + } + + addChallengeFromGoal(goal:Goal):Challenge { return null; } @@ -88,35 +90,31 @@ class Entity { return result; } - public getChallenges():string[] { - return this.currentChallenges; - } - public setChallenges(challenges:string[]):void { this.currentChallenges = challenges; } - public getFinishedBadges():BadgeIDsToNumberOfTimesEarnedMap { - return this.finishedBadgesMap; + public getBadges():BadgeIDsToNumberOfTimesEarnedMap { + return this.badgesMap; } - public getFinishedBadgesID():string[] { - return Object.keys(this.finishedBadgesMap); + public getBadgesID():string[] { + return Object.keys(this.badgesMap); } - public setFinishedBadges(finishedBadges:BadgeIDsToNumberOfTimesEarnedMap) { - this.finishedBadgesMap = finishedBadges; + public setBadges(finishedBadges:BadgeIDsToNumberOfTimesEarnedMap) { + this.badgesMap = finishedBadges; } - public addFinishedBadge(badgeID:string) { + public addBadge(badgeID:string) { if (!badgeID) { throw new BadArgumentException('Can not add given badge to user' + this.getName() + '. Badge given is null'); } - if (this.finishedBadgesMap.hasOwnProperty(badgeID)) { - this.finishedBadgesMap[badgeID]++; + if (this.badgesMap.hasOwnProperty(badgeID)) { + this.badgesMap[badgeID]++; } else { - this.finishedBadgesMap[badgeID] = 1; + this.badgesMap[badgeID] = 1; } } @@ -125,7 +123,7 @@ class Entity { id: this.id, name: this.name, currentChallenges: this.currentChallenges, - finishedBadgesMap: this.finishedBadgesMap + finishedBadgesMap: this.badgesMap } } } diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 9ff2fdc..08161ff 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -5,54 +5,17 @@ import Entity = require('./Entity'); import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); import User = require('./User'); -class Team { - private id:string; - private name:string; - +class Team extends Entity { private members:User[] = []; private leader:User; - private currentChallenges:string[] = []; - private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; - - constructor(name:string, leader:User, members:User[], currentChallenges:string[], badgesMap:BadgeIDsToNumberOfTimesEarnedMap, id = null) { - - if (name == null) { - throw new BadArgumentException('Can not build team, given name is null'); - } - - if (leader == null) { - throw new BadArgumentException('Can not build team ' + name + ' given leader is null'); - } + constructor(id:string, name:string, leader:User, members:User[], + currentChallenges:string[], badgesMap:BadgeIDsToNumberOfTimesEarnedMap) { - if (members == null) { - throw new BadArgumentException('Can not build team ' + name + ' given members is null'); - } - - if (currentChallenges == null) { - throw new BadArgumentException('Can not build team ' + name + ' given current challenges are null'); - } - - if (badgesMap == null) { - throw new BadArgumentException('Can not build team ' + name + ' given badges map is null'); - } - - this.id = (id == null) ? uuid.v4() : id; - this.name = name; + super(id, name, currentChallenges, badgesMap); this.leader = leader; this.members = members; - - this.currentChallenges = currentChallenges; - this.badgesMap = badgesMap; - } - - getUUID() { - return this.id; - } - - getName():string { - return this.name; } getLeader():User { @@ -63,17 +26,6 @@ class Team { return this.members; } - getBadges():BadgeIDsToNumberOfTimesEarnedMap { - return this.badgesMap; - } - hasUUID(aUUID:string):boolean { - return this.id === aUUID; - } - - hasName(name:string):boolean { - return this.getName() === name; - } - hasLeader(aUserID:string):boolean { return this.leader.hasUUID(aUserID); } @@ -89,36 +41,6 @@ class Team { return false; } - addChallenge(challengeID:string):void { - this.currentChallenges.push(challengeID); - } - - deleteChallenge(challengeID:string):void { - var challengeIndex:number = this.getChallenge(challengeID); - if (challengeIndex == -1) { - throw new BadArgumentException('Can not find given challenge ID'); - } - else { - this.currentChallenges.splice(challengeIndex, 1); - } - } - - getChallenge(challengeID:string):number { - var result:number = -1; - - for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { - if (this.currentChallenges[currentChallengeIndex] === challengeID) { - result = currentChallengeIndex; - } - } - - return result; - } - - getCurrentChallenges():string[] { - return this.currentChallenges; - } - getStringDescription():string { return 'Team:#' + this.getUUID() + '\t|Name : ' + this.getName() + '\t|LEADER : ' + this.leader + '\n'; } diff --git a/backend/src/user/TeamFactory.ts b/backend/src/user/TeamFactory.ts index c469c77..4a4fbef 100644 --- a/backend/src/user/TeamFactory.ts +++ b/backend/src/user/TeamFactory.ts @@ -1,11 +1,37 @@ +import uuid = require('node-uuid'); + import Entity = require('./Entity'); import Team = require('./Team'); import User = require('./User'); import UserRepository = require('./UserRepository'); +/* + + if (name == null) { + throw new BadArgumentException('Can not build team, given name is null'); + } + + if (leader == null) { + throw new BadArgumentException('Can not build team ' + name + ' given leader is null'); + } + + if (members == null) { + throw new BadArgumentException('Can not build team ' + name + ' given members is null'); + } + + if (currentChallenges == null) { + throw new BadArgumentException('Can not build team ' + name + ' given current challenges are null'); + } + + if (badgesMap == null) { + throw new BadArgumentException('Can not build team ' + name + ' given badges map is null'); + } + */ class TeamFactory { public createTeam(data:any, userRepository:UserRepository):Team { var teamID:string = data.id; + teamID = (teamID == null) ? uuid.v4() : teamID; + var teamName:string = data.name; var currentChallenges:string[] = data.currentChallenges; @@ -23,7 +49,7 @@ class TeamFactory { var leaderID:string = data.leader; var leader = userRepository.getUser(leaderID); - var team:Team = new Team(teamName, teamID, currentChallenges, finishedBadgesMap, members, leader); + var team:Team = new Team(teamID, teamName, leader, members, currentChallenges, finishedBadgesMap); return team; } } diff --git a/backend/src/user/TeamRepository.ts b/backend/src/user/TeamRepository.ts index 1baa7e3..e9bab4d 100644 --- a/backend/src/user/TeamRepository.ts +++ b/backend/src/user/TeamRepository.ts @@ -28,9 +28,9 @@ class TeamRepository { getTeamsByMember(aUserID:string):Team[] { var teams:Team[] = []; - for(var currentTeamIndex in this.teams) { + for (var currentTeamIndex in this.teams) { var team = this.teams[currentTeamIndex]; - if(team.hasMember(aUserID)) { + if (team.hasMember(aUserID)) { teams.push(team); } } @@ -38,10 +38,10 @@ class TeamRepository { return teams; } - hasMember(aUserID:string) :boolean { - for(var currentTeamIndex in this.teams) { + hasMember(aUserID:string):boolean { + for (var currentTeamIndex in this.teams) { var team = this.teams[currentTeamIndex]; - if(team.hasMember(aUserID)) { + if (team.hasMember(aUserID)) { return true; } } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index e382de4..5df4771 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -19,10 +19,10 @@ class User { private id; private name:string; - private mapSymbolicNameToSensor:any = {}; private currentChallenges:string[] = []; private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; + private mapSymbolicNameToSensor:any = {}; private challengeFactory:ChallengeFactory; constructor(name:string, mapSymbolicNameToSensor:any, currentChallenges:string[], @@ -91,7 +91,7 @@ class User { var newChallenge = this.challengeFactory.createChallenge(goal, this, now); // Check if we try - if (newChallenge.getEndDate().isAfter(goal.getEndDate())) { + if (newChallenge.getEndDate().isAfter(goal.getEndOfValidityPeriod())) { return null; } diff --git a/backend/tests/TeamTest.ts b/backend/tests/TeamTest.ts index 27c5a15..8a1e6c7 100644 --- a/backend/tests/TeamTest.ts +++ b/backend/tests/TeamTest.ts @@ -10,18 +10,25 @@ var assert = chai.assert; import Team = require('../src/user/Team'); import User = require('../src/user/User'); -describe("Teast a team", function () { +describe("Test a team", function () { var aMember:User; + var aMapSymoblicNameToSensor = { + 'TMP_CLI': 'AC_555' + }; + var anotherMember:User; + var anotherMapSymoblicNameToSensor = { + 'TMP_CLI': 'AC_666' + }; var members:User[] = []; var team:Team; beforeEach(() => { - aMember = new User('Gégé'); - anotherMember = new User('Dédé'); + aMember = new User('Gégé', aMapSymoblicNameToSensor, [], [], null); + anotherMember = new User('Dédé', anotherMapSymoblicNameToSensor, [], [], null); members = [aMember, anotherMember]; - team = new Team("Croquette", null, [], [], members, aMember); + team = new Team("id", "Croquette", aMember, members, [], null); }); describe('Check its composition', () => { @@ -38,23 +45,23 @@ describe("Teast a team", function () { describe('Check add method', () => { it('should have a challenge when it was previously added', () => { - chai.expect(team.getChallenges()).to.be.eqls([]); + chai.expect(team.getCurrentChallenges()).to.be.eqls([]); var aChallengeID = 'aChallengeID'; team.addChallenge(aChallengeID); - chai.expect(team.getChallenges()).to.be.eqls([aChallengeID]); + chai.expect(team.getCurrentChallenges()).to.be.eqls([aChallengeID]); }); it('should have added a challenge to its members when it was previously added', () => { - chai.expect(aMember.getChallenges()).to.be.eqls([]); - chai.expect(anotherMember.getChallenges()).to.be.eqls([]); + chai.expect(aMember.getCurrentChallenges()).to.be.eqls([]); + chai.expect(anotherMember.getCurrentChallenges()).to.be.eqls([]); var aChallengeID = 'aChallengeID'; team.addChallenge(aChallengeID); - chai.expect(aMember.getChallenges()).to.be.eqls([aChallengeID]); - chai.expect(anotherMember.getChallenges()).to.be.eqls([aChallengeID]); + chai.expect(aMember.getCurrentChallenges()).to.be.eqls([aChallengeID]); + chai.expect(anotherMember.getCurrentChallenges()).to.be.eqls([aChallengeID]); }); }); }); diff --git a/backend/tests/UserTest.ts b/backend/tests/UserTest.ts index 0eb74a0..3b71f73 100644 --- a/backend/tests/UserTest.ts +++ b/backend/tests/UserTest.ts @@ -10,50 +10,50 @@ var assert = chai.assert; import User = require('../src/user/User'); describe("Build a User", function () { - var user:User; + var user:User; - it("should have given name", () => { - user = new User("aName"); - assert.equal(user.getName(), "aName"); - }); + it("should have given name", () => { + user = new User("aName", null, [], null, null); + assert.equal(user.getName(), "aName"); + }); }); describe("Add a goal", function () { - var user:User; - - beforeEach(() => { - user = new User("aName"); - }); - - it("should throw an error if given goal is null", () => { - chai.expect(() => user.addChallenge(null)).to.throw(Error); - }); - - it("should add the goal to the user's description", () => { - /* - user.addGoalByDescription(new Goal("a", "a", 0)); - chai.expect(user.getGoals().length).to.be.equals(1); - */ - }); - it("should add the given goal to the user's description", () => { - /* - var goal:Goal = new Goal("a", "a", 0); - user.addGoalByDescription(goal); - chai.expect(user.getGoals().pop()).to.be.equals(goal); - */ - }); + var user:User; + + beforeEach(() => { + user = new User("aName", null, [], null, null); + }); + + it("should throw an error if given goal is null", () => { + chai.expect(() => user.addChallenge(null, null)).to.throw(Error); + }); + + it("should add the goal to the user's description", () => { + /* + user.addGoalByDescription(new Goal("a", "a", 0)); + chai.expect(user.getGoals().length).to.be.equals(1); + */ + }); + it("should add the given goal to the user's description", () => { + /* + var goal:Goal = new Goal("a", "a", 0); + user.addGoalByDescription(goal); + chai.expect(user.getGoals().pop()).to.be.equals(goal); + */ + }); }); describe("evaluate a goal", function () { - var user:User; + var user:User; - beforeEach(() => { - user = new User("aName"); - }); + beforeEach(() => { + user = new User("aName", null, [], null, null); + }); - it("should return false if given goalName doesn't exist", () => { - // var goal:Goal = new Goal("a", "inf", 10); - //user.addGoalByDescription(goal); - //assert.isFalse(user.evaluateGoal("aNameThatDoesntExist", 0)); - }); + it("should return false if given goalName doesn't exist", () => { + // var goal:Goal = new Goal("a", "inf", 10); + //user.addGoalByDescription(goal); + //assert.isFalse(user.evaluateGoal("aNameThatDoesntExist", 0)); + }); }); diff --git a/backend/tests/challenge/ChallengeFactoryTest.ts b/backend/tests/challenge/ChallengeFactoryTest.ts deleted file mode 100644 index 8124596..0000000 --- a/backend/tests/challenge/ChallengeFactoryTest.ts +++ /dev/null @@ -1,122 +0,0 @@ -/// -/// -/// -/// -/// -/// - -var moment = require('moment'); -var moment_timezone = require('moment-timezone'); - -import chai = require('chai'); -import sinon = require('sinon'); -var assert = chai.assert; - -import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); -import GoalRepository = require('../../src/goal/GoalRepository'); -import Goal = require('../../src/goal/Goal'); -import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); -import GoalExpression = require('../../src/condition/expression/GoalExpression'); -import Operand = require('../../src/condition/expression/Operand'); -import TimeBox = require('../../src/TimeBox'); -import Clock = require('../../src/Clock'); - -describe("GoalInstanceFactory test", () => { - - var factory:ChallengeFactory = new ChallengeFactory(); - var goalDefinitionRepository:GoalRepository = new GoalRepository(null); - - var aGoal:Goal; - var aGoalName:string = "goal 1"; - - var data:any; - - var aGoalID:string = "5d34ae6e-e9ca-4352-9a67-3fdf205cce26"; - - var aGoalName:string = 'badge 1'; - var aGoalDescription:string = 'a desc'; - - var aConditionName:string = 'Temp_cli'; - var aSensorName:string = 'AC_443'; - - var anotherConditionName:string = 'Temp_ext'; - var anotherSensorName:string = 'TEMP_443'; - var now:moment.Moment = moment(new Date(Clock.getNow()).valueOf()); - var endDate:moment.Moment = moment(new Date(now.year(), now.month(), now.date() + 5, now.hours(), now.minutes(), now.seconds()).valueOf()); - - - var conditions:any = {}; - beforeEach(() => { - - aGoal = new Goal(aGoalName, now, endDate, 5, null); - aGoal.setUUID(aGoalID); - - var goalCondition:OverallGoalCondition = new OverallGoalCondition(null, new GoalExpression(new Operand(aConditionName, true), '<', - new Operand(anotherConditionName, true), aGoalDescription), 0, null, null, null); - - aGoal.addCondition(goalCondition); - - goalDefinitionRepository.addGoal(aGoal); - - data = {}; - - data.id = aGoal.getUUID(); - - data.description = aGoalDescription; - - conditions[aConditionName] = aSensorName; - conditions[anotherConditionName] = anotherSensorName; - - data.goal = {}; - - data.goal.id = aGoal.getUUID(); - data.goal.conditions = conditions; - }); - - it("should have proper name when built", () => { - /* - var goalInstance = factory.createGoalInstance(data, goalDefinitionRepository, null, now); - chai.expect(goalInstance.getName()).to.be.equal(aGoalName); - */ - }); - - it("should have proper description when built", () => { - /* - var goalInstance = factory.createGoalInstance(data, goalDefinitionRepository, null, now); - chai.expect(goalInstance.getDescription()).to.be.equal(aGoalDescription); - */ - }); - - it("should have proper sensors when build", () => { - /* - var goalInstance = factory.createGoalInstance(data, goalDefinitionRepository, null, now); - - var timeBox:TimeBox = new TimeBox(now, endDate); - - var timeBoxDesc:any = {}; - timeBoxDesc.startDate = timeBox.getStartDateInStringFormat(); - timeBoxDesc.endDate = timeBox.getEndDateInStringFormat(); - - var expectedConditionsDescription = {}; - expectedConditionsDescription[aSensorName] = timeBoxDesc; - expectedConditionsDescription[anotherSensorName] = timeBoxDesc; - - chai.expect(goalInstance.getSensors()).to.be.eqls(expectedConditionsDescription); - */ - }); - - it('should have proper startDate when built', () => { - /* - var goalInstance = factory.createGoalInstance(data, goalDefinitionRepository, null, now); - chai.expect(goalInstance.getStartDate()).to.be.eq(now); - */ - }); - - it('should have proper endDate when built', () => { - /* - var goalInstance = factory.createGoalInstance(data, goalDefinitionRepository, null, now); - var aEndDate:Date = new Date(now.getFullYear(), now.getMonth(), now.getDate() + aGoal.getDuration(), now.getHours(), now.getMinutes(), now.getSeconds()); - chai.expect(goalInstance.getEndDate().getTime()).to.be.eq(aEndDate.getTime()); - */ - }); -}); \ No newline at end of file diff --git a/backend/tests/challenge/ChallengeTest.ts b/backend/tests/challenge/ChallengeTest.ts deleted file mode 100644 index 97acb28..0000000 --- a/backend/tests/challenge/ChallengeTest.ts +++ /dev/null @@ -1,115 +0,0 @@ -/// -/// -/// -/// -/// -/// - -var moment = require('moment'); -var moment_timezone = require('moment-timezone'); - - -import chai = require('chai'); -import sinon = require('sinon'); -var assert = chai.assert; - -import Challenge = require('../../src/challenge/UserChallenge'); -import Goal = require('../../src/goal/Goal'); -import GoalExpression = require('../../src/condition/expression/GoalExpression'); -import Operand = require('../../src/condition/expression/Operand'); -import AverageOnValue = require('../../src/condition/AverageOnValue'); -import TimeBox = require('../../src/TimeBox'); -import Clock = require('../../src/Clock'); - -describe("GoalInstance test", () => { - - var goalInstance:Challenge; - var goalDefinition:Goal; - - var aStartDate:moment.Moment = Clock.getMomentFromString('2000-05-01T00:00:00'); - var aDateOfCreation:moment.Moment = Clock.getMomentFromString('2000-05-01T00:00:00'); - var aEndDate:moment.Moment = Clock.getMomentFromString('2000-08-01T00:00:00'); - - var aSymbolicName:string = 'Temperature_cli'; - var anotherSymbolicName:string = 'Temperature_ext'; - - var aSensorName:string = 'AC_443'; - var anotherSensorName:string = 'TEMP_444'; - - var anExpression:GoalExpression = new GoalExpression(new Operand(aSymbolicName, true), '<', new Operand('40', false), 'desc'); - var anotherExpression:GoalExpression = new GoalExpression(new Operand(anotherSymbolicName, true), '>', new Operand('25', false), 'desc'); - - var anAverageCondition:AverageOnValue = new AverageOnValue(null, anExpression, 10, aStartDate, aDateOfCreation, aEndDate, Clock.getMoment(new Date(0,1,0,0,0,0,0).getTime())); - var anotherAverageCondition:AverageOnValue = new AverageOnValue(null, anotherExpression, 10, aStartDate, aDateOfCreation, aEndDate, Clock.getMoment(new Date(0,1,0,0,0,0,0).getTime())); - - beforeEach(() => { - goalDefinition = new Goal("goal1", aStartDate, aEndDate, 100, null); - - goalDefinition.addCondition(anAverageCondition); - goalDefinition.addCondition(anotherAverageCondition); - - var mapSymbolicNameToSensor:any = {}; - mapSymbolicNameToSensor[aSymbolicName] = aSensorName; - mapSymbolicNameToSensor[anotherSymbolicName] = anotherSensorName; - - goalInstance = new Challenge(aStartDate, aEndDate, "the badge for noobs", goalDefinition, mapSymbolicNameToSensor); - }); - - - it("should return sensors required correctly", () => { - var expectedConditionsDescription = {}; - - var timeBox:any = {}; - timeBox.startDate = "2000-04-01 00:00:00"; - timeBox.endDate = "2000-08-01 00:00:00"; - - expectedConditionsDescription[aSensorName] = timeBox; - expectedConditionsDescription[anotherSensorName] = timeBox; - - var result = goalInstance.getSensors(); - - chai.expect(result).to.be.eqls(expectedConditionsDescription); - }); - - it("should call goal definition evaluate on evaluate method", () => { - var goalStubObj = sinon.stub(goalDefinition, "evaluate"); - goalStubObj.returns(true); - - var fakeParams:any = {'a': null, 'b': null}; - - goalInstance.evaluate(fakeParams); - - chai.assert(goalStubObj.calledOnce); - chai.assert(goalStubObj.calledWith(goalInstance.bindSymbolicNameToValue(fakeParams), goalInstance)); - }); - - /* - FIXME - it("should evaluate the goal instance as OK", () => { - var aStartDatePlus1Day:Date = new Date(Date.UTC(2000, 5, 2)); - - var timeBoxObj:TimeBox = new TimeBox(aStartDatePlus1Day, aEndDate); - - var correctValuesDescription = {}; - var valuesForASensorName = {'values': [{date:timeBoxObj.getStartDateInStringFormat(), value:35}]}; - correctValuesDescription[aSensorName] = valuesForASensorName; - - var valuesForAnotherSensorName = {'values': [{date:timeBoxObj.getStartDateInStringFormat(), value:27}]}; - correctValuesDescription[anotherSensorName] = valuesForAnotherSensorName; - - chai.expect(goalInstance.evaluate(correctValuesDescription)).to.be.true; - }); - - it("should evaluate the goal instance as KO", () => { - var incorrectValuesDescription = {}; - - var valuesForASensorName = {'values': [{value: 35}]}; - incorrectValuesDescription[aSensorName] = valuesForASensorName; - - var valuesForAnotherSensorName = {'values': [{value: 20}]}; - incorrectValuesDescription[anotherSensorName] = valuesForAnotherSensorName; - - chai.expect(goalInstance.evaluate(incorrectValuesDescription)).to.be.false; - }); - */ -}); \ No newline at end of file diff --git a/backend/tests/challenge/UserChallengeFactoryTest.ts b/backend/tests/challenge/UserChallengeFactoryTest.ts new file mode 100644 index 0000000..31b86ac --- /dev/null +++ b/backend/tests/challenge/UserChallengeFactoryTest.ts @@ -0,0 +1,124 @@ +/// +/// +/// +/// +/// +/// + +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + +import chai = require('chai'); +import sinon = require('sinon'); +var assert = chai.assert; + +import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); +import RecurringSession = require('../../src/goal/RecurringSession'); +import Goal = require('../../src/goal/Goal'); +import GoalExpression = require('../../src/condition/expression/GoalExpression'); +import User = require('../../src/user/User'); +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); +import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); + +describe("GoalInstanceFactory test", () => { + + var factory:ChallengeFactory = new ChallengeFactory(); + + var aGoalID:string = "5d34ae6e-e9ca-4352-9a67-3fdf205cce26"; + var aGoalName:string = "goal 1"; + var aBadgeID:string = 'badge 1'; + + var now:moment.Moment = moment('2015-08-26T00:00:00'); + var startDate:moment.Moment = moment("2015-08-17T00:00:00"); + var endDate:moment.Moment = moment("2015-09-17T23:59:59"); + + var aRecurringSession:RecurringSession = new RecurringSession('week'); + + var aGoal:Goal = new Goal(aGoalID, aGoalName, aBadgeID, now, endDate, aRecurringSession); + + var aSymbolicName:string = 'Temp_cli'; + var aSensorName:string = 'AC_443'; + + var mapSymbolicNameToSensor:any = {}; + mapSymbolicNameToSensor[aSymbolicName] = aSensorName; + var aUser:User = new User('Gérard', mapSymbolicNameToSensor, [], null, factory); + + var expression:GoalExpression; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: ">" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 80; + + expression = expressionFactory.createExpression(expressionDescription); + var condition = new OverallGoalCondition(aConditionID, aConditionDescription, expression, aThresholdRate, null); + + aGoal.addCondition(condition); + + + var userChallenge = factory.createChallenge(aGoal, aUser, now); + + describe('Constructor', () => { + it("should have proper name when built", () => { + chai.expect(userChallenge.getName()).to.be.equal(aGoalName); + }); + + it("should have proper user when built", () => { + chai.expect(userChallenge.getUser().getName()).to.be.equal(aUser.getName()); + }); + + + it('should have proper startDate when built', () => { + var expectedStartDate = moment('2015-08-24T00:00:00'); + // Because challenge taken during a session are "realigned" with this session + + chai.expect(userChallenge.getStartDate().toISOString()).to.be.eql(expectedStartDate.toISOString()); + + }); + + it('should have proper endDate when built', () => { + var expectedEndDate = moment('2015-08-28T23:59:59.999'); + // Because challenge taken during a session are "realigned" with this session + + chai.expect(userChallenge.getEndDate().toISOString()).to.be.eql(expectedEndDate.toISOString()); + }); + }); + + describe('GetSensor method', () => { + + var result = userChallenge.getSensors(); + + it("should have condition's id of goal, recorded", () => { + chai.expect(result[aConditionID]).not.to.be.null; + }); + + it("should have condition's symbolic names recorded", () => { + chai.expect(result[aConditionID].symbolicNames).to.be.eqls([aSymbolicName]); + }) + + it("should have condition's timeBox recorded", () => { + chai.expect(result[aConditionID].timeBox).not.to.be.null; + }); + + it("should have sensors array built", () => { + chai.expect(result[aConditionID].sensors).to.be.eqls([aSensorName]); + }); + + it("should have sensor entry built", () => { + chai.expect(result[aConditionID][aSensorName]).not.to.be.null; + }); + }); + +}); \ No newline at end of file diff --git a/backend/tests/challenge/UserChallengeTest.ts b/backend/tests/challenge/UserChallengeTest.ts new file mode 100644 index 0000000..a987bf8 --- /dev/null +++ b/backend/tests/challenge/UserChallengeTest.ts @@ -0,0 +1,98 @@ +/// +/// +/// +/// +/// +/// + +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + + +import chai = require('chai'); +import sinon = require('sinon'); +var assert = chai.assert; + +import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); +import RecurringSession = require('../../src/goal/RecurringSession'); +import Goal = require('../../src/goal/Goal'); +import GoalExpression = require('../../src/condition/expression/GoalExpression'); +import User = require('../../src/user/User'); +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); +import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); + +describe("UserChallenge test", () => { + + var factory:ChallengeFactory = new ChallengeFactory(); + + var aGoalID:string = "5d34ae6e-e9ca-4352-9a67-3fdf205cce26"; + var aGoalName:string = "goal 1"; + var aBadgeID:string = 'badge 1'; + + var now:moment.Moment = moment('2015-08-26T00:00:00'); + var startDate:moment.Moment = moment("2015-08-17T00:00:00"); + var endDate:moment.Moment = moment("2015-09-17T23:59:59"); + + var aRecurringSession:RecurringSession = new RecurringSession('week'); + + var aGoal:Goal = new Goal(aGoalID, aGoalName, aBadgeID, now, endDate, aRecurringSession); + + var aSymbolicName:string = 'Temp_cli'; + var aSensorName:string = 'AC_443'; + + var mapSymbolicNameToSensor:any = {}; + mapSymbolicNameToSensor[aSymbolicName] = aSensorName; + var aUser:User = new User('Gérard', mapSymbolicNameToSensor, [], null, factory); + + var expression:GoalExpression; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: ">" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 80; + + expression = expressionFactory.createExpression(expressionDescription); + var condition = new OverallGoalCondition(aConditionID, aConditionDescription, expression, aThresholdRate, null); + + aGoal.addCondition(condition); + + var userChallenge = factory.createChallenge(aGoal, aUser, now); + + describe('GetSensor method', () => { + + var result = userChallenge.getSensors(); + + it("should have condition's id of goal, recorded", () => { + chai.expect(result[aConditionID]).not.to.be.null; + }); + + it("should have condition's symbolic names recorded", () => { + chai.expect(result[aConditionID].symbolicNames).to.be.eqls([aSymbolicName]); + }) + + it("should have condition's timeBox recorded", () => { + chai.expect(result[aConditionID].timeBox).not.to.be.null; + }); + + it("should have sensors array built", () => { + chai.expect(result[aConditionID].sensors).to.be.eqls([aSensorName]); + }); + + it("should have sensor entry built", () => { + chai.expect(result[aConditionID][aSensorName]).not.to.be.null; + }); + }); + +}); \ No newline at end of file diff --git a/backend/tests/condition/AverageOnValueTest.ts b/backend/tests/condition/AverageOnValueTest.ts index 58bf335..de52395 100644 --- a/backend/tests/condition/AverageOnValueTest.ts +++ b/backend/tests/condition/AverageOnValueTest.ts @@ -19,54 +19,90 @@ import Operand = require('../../src/condition/expression/Operand'); import TimeBox = require('../../src/TimeBox'); import Clock = require('../../src/Clock'); import TimeBoxFactory = require('../../src/TimeBoxFactory'); - +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); +import Filter = require('../../src/filter/Filter'); +import ReferencePeriod = require('../../src/condition/ReferencePeriod'); describe('Test AverageOnValueTest', () => { + var aSymbolicName = "TMP_Cli"; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: "<" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + var expression:GoalExpression = expressionFactory.createExpression(expressionDescription); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 10; + var filterOfCondition:Filter = new Filter('all', ['all']); + var referencePeriod:ReferencePeriod = new ReferencePeriod(1, 'week'); + + var averageOnValue:AverageOnValue = new AverageOnValue(aConditionID, aConditionDescription, expression, aThresholdRate, filterOfCondition, referencePeriod); + + var startDateOfChallenge:moment.Moment = Clock.getMomentFromString("2000-01-07T00:00:00"); + var endDateOfChallenge:moment.Moment = Clock.getMomentFromString("2000-01-14T00:00:00"); + var conditionDescription = averageOnValue.getRequiredByCondition(startDateOfChallenge, endDateOfChallenge); + + describe('GetRequired method', () => { + it('should have proper symbolic names field', () => { + var expected = [aSymbolicName]; + var result = conditionDescription.symbolicNames; + chai.expect(result).to.be.eqls(expected); + }); - var averageOnValue:AverageOnValue; - - var expressionStub = sinon.createStubInstance(GoalExpression); - - - var leftOperand:Operand = new Operand('TMP_Cli', true); - var rightOperand:Operand = new Operand('15', false); - var typeOfComparison:string = '<'; //baisse - var typeOfComparisonUp:string = '>'; //hausse - var description:string = 'un test'; + it('should have the proper start date', ()=> { + var expectedStartDate:moment.Moment = startDateOfChallenge.clone().subtract(1, 'week'); + var result = conditionDescription.timeBox.start; + chai.expect(result.toISOString()).to.be.eql(expectedStartDate.toISOString()); + }); - var startDate:moment.Moment = moment(new Date(Date.UTC(2000, 1, 1)).getTime()); - var dateOfCreation:moment.Moment = moment(new Date(Date.UTC(2000, 1, 7)).getTime()); - var endDate:moment.Moment = moment(new Date(Date.UTC(2000, 1, 15)).getTime()); + it('should have the proper date of creation', ()=> { + var expectedDateOfCreation:moment.Moment = startDateOfChallenge.clone(); + var result = conditionDescription.timeBox.dateOfCreation; + chai.expect(result.toISOString()).to.be.eql(expectedDateOfCreation.toISOString()); + }); - var expression:GoalExpression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + it('should have the proper end date', ()=> { + var expectedEndDate:moment.Moment = endDateOfChallenge.clone(); + var result = conditionDescription.timeBox.end; + chai.expect(result.toISOString()).to.be.eql(expectedEndDate.toISOString()); + }); + }); describe('evaluate method decrease', () => { it('should return true if threshold is reached', () => { - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate, moment(new Date(0,1,0,0,0,0,0).getTime())); var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '' + '', + date: "2000-01-02T00:00:00", value: '100' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '' + '', + date: "2000-01-03T00:00:00", value: '101' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '' + '', + date: "2000-01-04T00:00:00", value: '99' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '' + '', + date: "2000-01-05T00:00:00", value: '102' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '' + '', + date: "2000-01-06T00:00:00", value: '98' } ]; @@ -74,136 +110,132 @@ describe('Test AverageOnValueTest', () => { var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '' + '', - value:'89' + date: "2000-01-08T00:00:00", + value: '89' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '' + '', + date: "2000-01-09T00:00:00", value: '90' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '' + '', + date: "2000-01-10T00:00:00", value: '91' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '' + '', + date: "2000-01-11T00:00:00", value: '70' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 12)).getTime()).valueOf() + '' + '', + date: "2000-01-12T00:00:00", value: '110' } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); - + data[aSymbolicName] = oldValues.concat(newValues); - chai.expect(averageOnValue.evaluate(data)).to.be.true; + var result:any = averageOnValue.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); it('should return true if threshold is reached with different number of measures', () => { - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); - var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', - value: 100 + date: "2000-01-02T00:00:00", + value: '100' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', - value: 101 + date: "2000-01-03T00:00:00", + value: '101' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', - value: 99 + date: "2000-01-04T00:00:00", + value: '99' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', - value: 102 + date: "2000-01-05T00:00:00", + value: '102' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', - value: 98 + date: "2000-01-06T00:00:00", + value: '98' } ]; var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', - value: 89 + date: "2000-01-08T00:00:00", + value: '89' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', - value: 90 + date: "2000-01-09T00:00:00", + value: '90' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', - value: 91 + date: "2000-01-10T00:00:00", + value: '91' } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); + data[aSymbolicName] = oldValues.concat(newValues); - chai.expect(averageOnValue.evaluate(data)).to.be.true; + var result:any = averageOnValue.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); it('should return false if threshold is close but not reached', () => { - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,Clock.getMoment(new Date(0,1,0,0,0,0,0).getTime())); var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', - value: 100 + date: "2000-01-02T00:00:00", + value: '100' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', - value: 101 + date: "2000-01-03T00:00:00", + value: '101' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', - value: 99 + date: "2000-01-04T00:00:00", + value: '99' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', - value: 102 + date: "2000-01-05T00:00:00", + value: '102' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', - value: 98 + date: "2000-01-06T00:00:00", + value: '98' } ]; + var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', - value: 89 + date: "2000-01-08T00:00:00", + value: '89' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', - value: 91 + date: "2000-01-09T00:00:00", + value: '91' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', - value: 91 + date: "2000-01-10T00:00:00", + value: '91' } ]; + data[aSymbolicName] = oldValues.concat(newValues); - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); + var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(averageOnValue.evaluate(data)).to.be.false; + chai.expect(result.finished).to.be.false; }); + }); describe('progression fields', () => { @@ -213,9 +245,6 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = []; var newValues:any[] = []; - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); - beforeEach(() => { data = {}; @@ -223,23 +252,23 @@ describe('Test AverageOnValueTest', () => { // average : 100 oldValues = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', + date: "2000-01-02T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', + date: "2000-01-03T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', + date: "2000-01-04T00:00:00", value: 99 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', + date: "2000-01-05T00:00:00", value: 102 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', + date: "2000-01-06T00:00:00", value: 98 } ]; @@ -249,25 +278,26 @@ describe('Test AverageOnValueTest', () => { // average : 100 newValues = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 99 } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); - averageOnValue.evaluate(data); + data[aSymbolicName] = oldValues.concat(newValues); - assert.equal(averageOnValue.getPercentageAchieved(), 0); + var result:any = averageOnValue.evaluate(data, conditionDescription); + + + assert.equal(result.percentageAchieved, 0); }); it('should have 50 percentage achieved', () => { @@ -275,124 +305,132 @@ describe('Test AverageOnValueTest', () => { // average : 95 newValues = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 90 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 95 } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); + data[aSymbolicName] = oldValues.concat(newValues); + + var result:any = averageOnValue.evaluate(data, conditionDescription); - averageOnValue.evaluate(data); - assert.equal(averageOnValue.getPercentageAchieved(), 50); + + assert.equal(result.percentageAchieved, 50); }); - it('should have 100 percentage achieved', () => { + it('should have 50 percentage achieved when old values must be ignored', () => { + + oldValues.push({ + date: "1999-12-10T00:00:00", + value: 110 + }); // average : 95 newValues = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', - value: 85 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', - value: 95 + date: "2000-01-08T00:00:00", + value: 90 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', - value: 91 + date: "2000-01-09T00:00:00", + value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '', - value: 89 + date: "2000-01-10T00:00:00", + value: 95 } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); + data[aSymbolicName] = oldValues.concat(newValues); - averageOnValue.evaluate(data); - assert.equal(averageOnValue.getPercentageAchieved(), 100); - }); - }); + var result:any = averageOnValue.evaluate(data, conditionDescription); - describe('duration achieved', () => { - beforeEach(() => { - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate, moment(new Date(0,1,0,0,0,0,0).getTime())); - }); - it('should be at 50 percent of time', () => { - var currentDate:moment.Moment = moment(new Date(Date.UTC(2000, 1, 11)).getTime()); - averageOnValue.updateDurationAchieved(currentDate.valueOf()); - assert.equal(averageOnValue.getPercentageOfTimeElapsed(), 50); + assert.equal(result.percentageAchieved, 50); }); - it('should be at 0 percent of time', () => { - var currentDate:moment.Moment = moment(new Date(Date.UTC(2000, 1, 7)).getTime()); - averageOnValue.updateDurationAchieved(currentDate.valueOf()); - assert.equal(averageOnValue.getPercentageOfTimeElapsed(), 0); - }); + it('should have 100 percentage achieved', () => { - it('should throw an error if time given is before dateOfCreation', () => { - var currentDate:moment.Moment = moment(new Date(Date.UTC(2000, 1, 1)).getTime()); - chai.expect(() => averageOnValue.updateDurationAchieved(currentDate.valueOf())).to.throw(Error); + // average : 95 + //85.95.91.89 + newValues = [ + { + date: "2000-01-08T00:00:00", + value: '85' + }, + { + date: "2000-01-09T00:00:00", + value: '95' + }, + { + date: "2000-01-10T00:00:00", + value: '91' + }, + { + date: "2000-01-11T00:00:00", + value: '89' + } + ]; + + data[aSymbolicName] = oldValues.concat(newValues); + + var result:any = averageOnValue.evaluate(data, conditionDescription); + + assert.equal(result.percentageAchieved, 100); }); }); + }); describe('separate data', () => { - it('should separate data correctly', () => { - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); + it('should separate data correctly', () => { var values:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', + date: "2000-01-02T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', + date: "2000-01-03T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', + date: "2000-01-04T00:00:00", value: 99 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', + date: "2000-01-05T00:00:00", value: 102 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', + date: "2000-01-06T00:00:00", value: 98 }, // OLD/NEW DATA { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 89 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 90 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 91 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '', + date: "2000-01-11T00:00:00", value: 70 } ]; @@ -404,331 +442,248 @@ describe('Test AverageOnValueTest', () => { var actualOldValues:number[] = []; var actualNewValues:number[] = []; - averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues); + averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, Clock.getMomentFromString("2000-01-07T00:00:00")); chai.expect(actualOldValues).to.be.eqls(expectedOldValues); chai.expect(actualNewValues).to.be.eqls(expectedNewValues); }); - it('should separate data if these are older than 20 days', () => { - var newStartDate:moment.Moment = moment(new Date(Date.UTC(1999, 12, 18)).getTime()); - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, newStartDate, dateOfCreation, endDate,moment(new Date(0,0,20,0,0,0,0).getTime())); + it('should separate data even if these are older than start date', () => { var values:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', + date: "2000-01-02T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', + date: "2000-01-03T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', + date: "2000-01-04T00:00:00", value: 99 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', + date: "2000-01-05T00:00:00", value: 102 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', + date: "2000-01-06T00:00:00", value: 98 }, // OLD/NEW DATA - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 89 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 90 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 91 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '', + date: "2000-01-11T00:00:00", value: 70 }, //too old data, so it won't be in the arrays { - date: Clock.getMoment(new Date(Date.UTC(1999, 12, 17)).getTime()).valueOf() + '', + date: "1999-11-17T00:00:00", value: 42 } ]; - var expectedOldValues:number[] = [100, 101, 99, 102, 98]; + var expectedOldValues:number[] = [100, 101, 99, 102, 98, 42]; var expectedNewValues:number[] = [89, 90, 91, 70]; var actualOldValues:number[] = []; var actualNewValues:number[] = []; - averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues); + averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, conditionDescription.timeBox.dateOfCreation); chai.expect(actualOldValues).to.be.eqls(expectedOldValues); chai.expect(actualNewValues).to.be.eqls(expectedNewValues); }); - it('should add data if these are younger than 30 days', () => { - var newStartDate:moment.Moment = Clock.getMoment(new Date(Date.UTC(1999, 12, 8)).getTime()); - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, newStartDate, dateOfCreation, endDate,moment(new Date(0,0,30,0,0,0,0).getTime())); - - var values:any[] = [ - - //datas after 1999-12-8 - { - date: Clock.getMoment(new Date(Date.UTC(1999, 12, 8, 1)).getTime()).valueOf() + '', - value: 95 - }, - { - date: Clock.getMoment(new Date(Date.UTC(1999, 12, 12)).getTime()).valueOf() + '', - value: 105 - }, - { - date: Clock.getMoment(new Date(Date.UTC(1999, 12, 31)).getTime()).valueOf() + '', - value: 100 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', - value: 100 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', - value: 101 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', - value: 99 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', - value: 102 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', - value: 98 - }, - - // OLD/NEW DATA - - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', - value: 89 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', - value: 90 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', - value: 91 - }, - { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '', - value: 70 - } - ]; - - - var expectedOldValues:number[] = [95,105,100,100, 101, 99, 102, 98]; - var expectedNewValues:number[] = [89, 90, 91, 70]; - - var actualOldValues:number[] = []; - var actualNewValues:number[] = []; - averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues); - chai.expect(actualOldValues).to.be.eqls(expectedOldValues); - chai.expect(actualNewValues).to.be.eqls(expectedNewValues); - }); }); - describe('getRequired method', () => { - - expression = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); - - var expected:any = {}; - var timeBoxDesc:any = {}; - timeBoxDesc['startDate'] = '2000-02-01 00:00:00'; - timeBoxDesc['endDate'] = '2000-02-15 00:00:00'; - expected['TMP_Cli'] = timeBoxDesc; - - console.log(JSON.stringify(expected)); - console.log(JSON.stringify(averageOnValue.getRequired())); - - //chai.expect(averageOnValue.getRequired()).to.be.eql(expected); - - }); describe('evaluate method increase', () => { + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: ">" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + var expression:GoalExpression = expressionFactory.createExpression(expressionDescription); + var averageOnValue:AverageOnValue = new AverageOnValue(aConditionID, aConditionDescription, expression, aThresholdRate, filterOfCondition, referencePeriod); + it('should return true if threshold is reached', () => { - expression = new GoalExpression(leftOperand, typeOfComparisonUp, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', - value: 100 + date: "2000-01-02T00:00:00", + value: '100' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', - value: 101 + date: "2000-01-03T00:00:00", + value: '101' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', - value: 99 + date: "2000-01-04T00:00:00", + value: '99' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', - value: 102 + date: "2000-01-05T00:00:00", + value: '102' }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', - value: 98 + date: "2000-01-06T00:00:00", + value: '98' } ]; var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 121 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 110 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 119 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 11)).getTime()).valueOf() + '', + date: "2000-01-11T00:00:00", value: 70 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 12)).getTime()).valueOf() + '', + date: "2000-01-12T00:00:00", value: 130 } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); - + data[aSymbolicName] = oldValues.concat(newValues); - chai.expect(averageOnValue.evaluate(data)).to.be.true; + var result:any = averageOnValue.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); it('should return true if threshold is reached with different number of measures', () => { - expression = new GoalExpression(leftOperand, typeOfComparisonUp, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate,moment(new Date(0,1,0,0,0,0,0).getTime())); - var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', + date: "2000-01-02T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', + date: "2000-01-03T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', + date: "2000-01-04T00:00:00", value: 99 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', + date: "2000-01-05T00:00:00", value: 102 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', + date: "2000-01-06T00:00:00", value: 98 } ]; var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 111 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 110 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '', + date: "2000-01-10T00:00:00", value: 109 } ]; + data[aSymbolicName] = oldValues.concat(newValues); - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); - - chai.expect(averageOnValue.evaluate(data)).to.be.true; + var result:any = averageOnValue.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); it('should return false if threshold is close but not reached', () => { - expression = new GoalExpression(leftOperand, typeOfComparisonUp, rightOperand, description); - averageOnValue = new AverageOnValue(null,expression,10, startDate, dateOfCreation, endDate, moment(new Date(0,1,0,0,0,0,0).getTime())); - var data:any = {}; var oldValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 2)).getTime()).valueOf() + '', + date: "2000-01-02T00:00:00", value: 100 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 3)).getTime()).valueOf() + '', + date: "2000-01-03T00:00:00", value: 101 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 4)).getTime()).valueOf() + '', + date: "2000-01-04T00:00:00", value: 99 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 5)).getTime()).valueOf() + '', + date: "2000-01-05T00:00:00", value: 102 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 6)).getTime()).valueOf() + '', + date: "2000-01-06T00:00:00", value: 98 } ]; var newValues:any[] = [ { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 8)).getTime()).valueOf() + '', + date: "2000-01-08T00:00:00", value: 111 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 9)).getTime()).valueOf() + '', + date: "2000-01-09T00:00:00", value: 109 }, { - date: Clock.getMoment(new Date(Date.UTC(2000, 1, 10)).getTime()).valueOf() + '' + '', + date: "2000-01-10T00:00:00" + '', value: 109 } ]; - data['TMP_Cli'] = {}; - data['TMP_Cli'].values = oldValues.concat(newValues); + data[aSymbolicName] = oldValues.concat(newValues); - chai.expect(averageOnValue.evaluate(data)).to.be.false; + var result:any = averageOnValue.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.false; }); + }); + + }); \ No newline at end of file diff --git a/backend/tests/condition/ConditionTest.ts b/backend/tests/condition/ConditionTest.ts index 4fd3fb3..5ef27fe 100644 --- a/backend/tests/condition/ConditionTest.ts +++ b/backend/tests/condition/ConditionTest.ts @@ -94,21 +94,26 @@ describe('Test Condition', () => { describe('KeepUsefulValues method', () => { it('should keep nothing if nothing is in the timeBox', () => { - var expected:any[] = []; - var data:any[] = [ + var expected:any = {}; + expected[aSymbolicName] = []; + + var data:any = {}; + data[aSymbolicName] = [ {date: "2000-09-01T00:00:00", value: 10}, {date: "2000-09-01T00:00:00", value: 10}, {date: "2000-09-01T00:00:00", value: 10}, {date: "2000-09-01T00:00:00", value: 10} ]; + + var result:any[] = condition.keepUsefulValues(data, conditionDescription); chai.expect(result).to.be.eqls(expected); }); it('should keep everything if everything is in the timeBox', () => { - var expected:any[] = []; - var data:any[] = [ + var data:any = {}; + data[aSymbolicName] = [ {date: "2000-02-01T00:00:00", value: 10}, {date: "2000-02-01T00:00:00", value: 10}, {date: "2000-02-01T00:00:00", value: 10}, @@ -116,12 +121,20 @@ describe('Test Condition', () => { ]; var result:any[] = condition.keepUsefulValues(data, conditionDescription); - chai.expect(result).to.be.eqls(expected); + chai.expect(result).to.be.eqls(data); }); it('should keep what is in the timeBox', () => { - var expected:any[] = []; - var data:any[] = [ + var expected:any = {}; + expected[aSymbolicName] = [ + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10} + ]; + + var data:any = {}; + data[aSymbolicName] = [ {date: "1999-01-01T00:00:00", value: 10}, {date: "2000-02-01T00:00:00", value: 10}, {date: "2000-02-01T00:00:00", value: 10}, @@ -146,7 +159,7 @@ describe('Test Condition', () => { end: anEnd } }; - var result = condition.getRequiredByCondition(aStart, anEnd, null); + var result = condition.getRequiredByCondition(aStart, anEnd); chai.expect(result).to.be.eqls(expected); }); }); diff --git a/backend/tests/condition/OverallGoalConditionTest.ts b/backend/tests/condition/OverallGoalConditionTest.ts index 97b4443..9f650af 100644 --- a/backend/tests/condition/OverallGoalConditionTest.ts +++ b/backend/tests/condition/OverallGoalConditionTest.ts @@ -19,119 +19,124 @@ import TimeBox = require('../../src/TimeBox'); import Clock = require('../../src/Clock'); import TimeBoxFactory = require('../../src/TimeBoxFactory'); -describe('Test OverallGoalCondition', () => { - - var overallGoalCondition:OverallGoalCondition; - - var condition:GoalExpression; +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); +import Filter = require('../../src/filter/Filter'); - var leftOperand:Operand = new Operand('TMP_Cli', true); - var rightOperand:Operand = new Operand('15', false); - var typeOfComparison:string = '>'; - var description:string = 'un test'; +describe('Test OverallGoalCondition', () => { - var startDate:moment.Moment = moment(new Date(Date.UTC(2000,1,1)).getTime()); - var endDate:moment.Moment = moment(new Date(Date.UTC(2000,8,1)).getTime()); + var aSymbolicName = "TMP_CLI"; + var expression:GoalExpression; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: ">" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 80; + var filterOfCondition:Filter = new Filter('all', ['all']); + + expression = expressionFactory.createExpression(expressionDescription); + var condition = new OverallGoalCondition(aConditionID, aConditionDescription, expression, aThresholdRate, filterOfCondition); + var conditionDescription:any = { + symbolicNames: ["TMP_CLI"], + timeBox: { + start: Clock.getMomentFromString("2000-01-01T00:00:00"), + end: Clock.getMomentFromString("2000-08-01T00:00:00") + } + }; it('should return false if min threshold is absolutely not reached', () => { - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - overallGoalCondition = new OverallGoalCondition(null, condition, 80, startDate, moment(new Date(Clock.getNow()).getTime()), endDate); - var data:any = {}; var values:any[] = [ - {date:null,value:10}, - {date:null,value:10}, - {date:null,value:10}, - {date:null,value:10}, - {date:null,value:10}, - {date:null,value:16}, - {date:null,value:18} + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 16}, + {date: "2000-02-01T00:00:00", value: 18} ]; - var valuesDesc:any = {}; - valuesDesc['values'] = values; - - data['TMP_Cli'] = valuesDesc; + data[aSymbolicName] = values; - chai.expect(overallGoalCondition.evaluate(data)).to.be.false; + var result = condition.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.false; }); it('should return false if min threshold is not reached', () => { - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - overallGoalCondition = new OverallGoalCondition(null, condition, 80, startDate, moment(new Date(Clock.getNow()).getTime()), endDate); - var data:any = {}; var values:any[] = [ - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:10}, - {date:null,value:10}, - {date:null,value:10} + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10} ]; - var valuesDesc:any = {}; - valuesDesc['values'] = values; + data[aSymbolicName] = values; - data['TMP_Cli'] = valuesDesc; - chai.expect(overallGoalCondition.evaluate(data)).to.be.false; + var result = condition.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.false; }); it('should return true if min threshold is just reached', () => { - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - overallGoalCondition = new OverallGoalCondition(null, condition, 50, startDate, moment(new Date(Clock.getNow()).getTime()), endDate); - var data:any = {}; var values:any[] = [ - {date:null,value:17}, - {date:null,value:16}, - {date:null,value:16}, - {date:null,value:17}, - {date:null,value:18}, - {date:null,value:19}, - {date:null,value:18}, - {date:null,value:17}, - {date:null,value:10}, - {date:null,value:10} + {date: "2000-02-01T00:00:00", value: 17}, + {date: "2000-02-01T00:00:00", value: 16}, + {date: "2000-02-01T00:00:00", value: 16}, + {date: "2000-02-01T00:00:00", value: 17}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 19}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 17}, + {date: "2000-02-01T00:00:00", value: 10}, + {date: "2000-02-01T00:00:00", value: 10} ]; - var valuesDesc:any = {}; - valuesDesc['values'] = values; + data[aSymbolicName] = values; - data['TMP_Cli'] = valuesDesc; - // FIXME DATE NULL chai.expect(overallGoalCondition.evaluate(data)).to.be.true; + var result = condition.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); - it('should return true if min threshold is reached', () => { - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); - overallGoalCondition = new OverallGoalCondition(null, condition, 50, startDate, moment(new Date(Clock.getNow()).getTime()), endDate); + it('should return true if min threshold is reached', () => { var data:any = {}; var values:any[] = [ - {date:null,value:16}, - {date:null,value:17}, - {date:null,value:18}, - {date:null,value:19}, - {date:null,value:18}, - {date:null,value:18}, - {date:null,value:17}, - {date:null,value:18}, - {date:null,value:16}, - {date:null,value:17} + {date: "2000-02-01T00:00:00", value: 16}, + {date: "2000-02-01T00:00:00", value: 17}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 19}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 17}, + {date: "2000-02-01T00:00:00", value: 18}, + {date: "2000-02-01T00:00:00", value: 16}, + {date: "2000-02-01T00:00:00", value: 17} ]; - var valuesDesc:any = {}; - valuesDesc['values'] = values; - - data['TMP_Cli'] = valuesDesc; + data[aSymbolicName] = values; - // FIXME DATE NULL chai.expect(overallGoalCondition.evaluate(data)).to.be.true; + var result = condition.evaluate(data, conditionDescription); + chai.expect(result.finished).to.be.true; }); }); \ No newline at end of file diff --git a/backend/tests/condition/expression/GoalExpressionTest.ts b/backend/tests/condition/expression/GoalExpressionTest.ts index 1b94487..6e350d5 100644 --- a/backend/tests/condition/expression/GoalExpressionTest.ts +++ b/backend/tests/condition/expression/GoalExpressionTest.ts @@ -25,42 +25,42 @@ describe('Test GoalExpression', () => { var description:string = 'un test'; it('should not have required', () => { - goalExpression = new GoalExpression(leftOperandNotRequired, typeOfComparison, rightOperandNotRequired, description); + goalExpression = new GoalExpression(leftOperandNotRequired, typeOfComparison, rightOperandNotRequired); var expected:string[] = []; chai.expect(goalExpression.getRequired()).to.be.eqls(expected); }); it('should have proper required on left operand', () => { - goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandNotRequired, description); + goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandNotRequired); var expected:string[] = ['TMP_Cli']; chai.expect(goalExpression.getRequired()).to.be.eqls(expected); }); it('should have proper required on right operand', () => { - goalExpression = new GoalExpression(leftOperandNotRequired, typeOfComparison, rightOperandRequired, description); + goalExpression = new GoalExpression(leftOperandNotRequired, typeOfComparison, rightOperandRequired); var expected:string[] = ['10']; chai.expect(goalExpression.getRequired()).to.be.eqls(expected); }); it('should have proper required on both operand', () => { - goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired, description); + goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired); var expected:string[] = ['TMP_Cli', '10']; chai.expect(goalExpression.getRequired()).to.be.eqls(expected); }); it('should have proper left operand', () => { - goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired, description); + goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired); chai.expect(goalExpression.hasLeftOperand('TMP_Cli')).to.be.true; }); it('should have proper right operand', () => { - goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired, description); + goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired); chai.expect(goalExpression.hasRightOperand('10')).to.be.true; }); it('should have proper type of comparison', () => { - goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired, description); + goalExpression = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandRequired); chai.expect(goalExpression.getComparisonType()).to.be.eq(typeOfComparison); }); @@ -79,7 +79,7 @@ describe('Test GoalExpression', () => { typeOfComparison = '<'; description = 'un test'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should evaluate basic boolean comparison should not throw error', () => { @@ -201,7 +201,7 @@ describe('Test GoalExpression', () => { typeOfComparison = '>'; description = 'un test'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); describe('Evaluate with >', () => { @@ -289,7 +289,7 @@ describe('Test GoalExpression', () => { typeOfComparison = '<'; description = 'un test'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); describe('Evaluate with <', () => { @@ -307,7 +307,7 @@ describe('Test GoalExpression', () => { describe('Evaluate with >', () => { beforeEach(() => { typeOfComparison = '>'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should evaluate at true with 10', () => { @@ -324,7 +324,7 @@ describe('Test GoalExpression', () => { describe('Evaluate with ==', () => { beforeEach(() => { typeOfComparison = '=='; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should evaluate at true with 15', () => { @@ -341,7 +341,7 @@ describe('Test GoalExpression', () => { describe('Evaluate with !=', () => { beforeEach(() => { typeOfComparison = '!='; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should evaluate at true with 10', () => { @@ -358,7 +358,7 @@ describe('Test GoalExpression', () => { describe('Evaluate with a UNKNOWN field tagged as non required', () => { it('should throw an error when evaluate with 15 { rightOperand = new Operand('TMP_CLIM', false); - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); chai.expect(() => condition.evaluate([])).to.throw(Error); }); }); @@ -377,7 +377,7 @@ describe('Test GoalExpression', () => { typeOfComparison = '>'; description = 'un test'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should evaluate at true when 20,10 are passed', () => { @@ -412,7 +412,7 @@ describe('Test GoalExpression', () => { typeOfComparison = '>'; description = 'un test'; - condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand, description); + condition = new GoalExpression(leftOperand, typeOfComparison, rightOperand); }); it('should throw an error if left operand is required and not provided', () => { @@ -422,7 +422,7 @@ describe('Test GoalExpression', () => { }); it('should throw an error if right operand is required and not provided', () => { - condition = new GoalExpression(rightOperand, typeOfComparison, leftOperand, description); + condition = new GoalExpression(rightOperand, typeOfComparison, leftOperand); chai.expect(() => { condition.evaluate({TMP_CLIIIIIIIII: 20}) }).to.throw(BadArgumentException); @@ -444,7 +444,7 @@ describe('Test GoalExpression', () => { var typeOfComparison:string = '<'; var description:string = 'un test'; - condition = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandNotRequired, description); + condition = new GoalExpression(leftOperandRequired, typeOfComparison, rightOperandNotRequired); it('should return correct protocol', () => { var expected:string[] = ['TMP_CLI']; diff --git a/backend/tests/database/GoalConditionStoreTest.ts b/backend/tests/database/GoalConditionStoreTest.ts index 1f28a8d..e7d3fc1 100644 --- a/backend/tests/database/GoalConditionStoreTest.ts +++ b/backend/tests/database/GoalConditionStoreTest.ts @@ -25,16 +25,16 @@ describe('Test store GoalCondition class', () => { var description = 'a desc'; var goalExpression:GoalExpression = new GoalExpression(leftOperand, typeOfComparison, - rightOperand, description); + rightOperand); var expected:any = { valueLeft: { value: leftOperand.getStringDescription(), - sensor: leftOperand.hasToBeDefined() + symbolicName: leftOperand.hasToBeDefined() }, valueRight: { value: rightOperand.getStringDescription(), - sensor: rightOperand.hasToBeDefined() + symbolicName: rightOperand.hasToBeDefined() }, comparison: typeOfComparison, description: description @@ -69,8 +69,5 @@ describe('Test store GoalCondition class', () => { chai.expect(goalConditionClone.getComparisonType()).to.be.eq(typeOfComparison); }); - it('should have the same description', () => { - chai.expect(goalConditionClone.getDescription()).to.be.eq(description); - }); }); }); \ No newline at end of file diff --git a/backend/tests/database/UserStoreTest.ts b/backend/tests/database/UserStoreTest.ts index 5a46ddd..c1e7ad7 100644 --- a/backend/tests/database/UserStoreTest.ts +++ b/backend/tests/database/UserStoreTest.ts @@ -12,16 +12,21 @@ describe('Test store user class', () => { var aName:string = 'aName'; var anID:string = 'anID'; + var mapSymbolicNameToSensor:any = { + 'TMP_CLI':'AC_444' + }; + var currentChallenges:string[] = ['c1', 'c2']; var finishedBadgesMap:any = { 'b1':2, 'b2':4 }; - var user:User = new User(aName, anID, currentChallenges, finishedBadgesMap); + var user:User = new User(aName,mapSymbolicNameToSensor, currentChallenges, finishedBadgesMap, null, anID); var expected:any = { id:anID, name:aName, + mapSymbolicNameToSensor: mapSymbolicNameToSensor, currentChallenges:currentChallenges, finishedBadgesMap:finishedBadgesMap }; @@ -31,7 +36,7 @@ describe('Test store user class', () => { }); describe('build with its own description', () => { - var userClone:User = new User(expected.name, expected.id, expected.currentChallenges, expected.finishedBadgesMap); + var userClone:User = new User(expected.name,expected.mapSymbolicNameToSensor,expected.currentChallenges, expected.finishedBadgesMap, expected.id); it('should have the same name', () => { chai.expect(userClone.getName()).to.be.eq(aName); @@ -42,7 +47,7 @@ describe('Test store user class', () => { }); it('should have the same current challenges', () => { - chai.expect(userClone.getChallenges()).to.be.eq(currentChallenges); + chai.expect(userClone.getCurrentChallenges()).to.be.eq(currentChallenges); }); it('should have the same finished badges map', () => { diff --git a/backend/tests/goal/GoalFactoryTest.ts b/backend/tests/goal/GoalFactoryTest.ts index ada9d37..32b9598 100644 --- a/backend/tests/goal/GoalFactoryTest.ts +++ b/backend/tests/goal/GoalFactoryTest.ts @@ -15,13 +15,16 @@ describe("GoalFactory test", () => { var factory:GoalFactory = new GoalFactory(); var goal:Goal; + var start = Clock.getMomentFromString("2000-01-01T00:00:00"); + var end = Clock.getMomentFromString("2000-08-01T00:00:00"); + beforeEach(() => { var jsonDefinition:any = {}; jsonDefinition.name = "Clim"; var timeBox:any = {}; - timeBox.startDate = Clock.getCurrentMoment(); - timeBox.endDate = Clock.getCurrentMoment().add(10,'minutes'); + timeBox.startDate = start.toISOString(); + timeBox.endDate = end.toISOString(); jsonDefinition.timeBox = timeBox; jsonDefinition.duration = 'day'; @@ -53,6 +56,6 @@ describe("GoalFactory test", () => { }); it("should build a goal with non null conditions", () => { - chai.expect(goal.getRequired()).to.be.not.null; + chai.expect(goal.getRequired(start, end)).to.be.not.null; }); }); \ No newline at end of file diff --git a/backend/tests/goal/GoalTest.ts b/backend/tests/goal/GoalTest.ts index 4c0a682..a082d3d 100644 --- a/backend/tests/goal/GoalTest.ts +++ b/backend/tests/goal/GoalTest.ts @@ -14,61 +14,98 @@ var assert = chai.assert; import Goal = require('../../src/goal/Goal'); -import ConditionList = require('../../src/condition/ConditionList'); import GoalExpression = require('../../src/condition/expression/GoalExpression'); import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); import Operand = require('../../src/condition/expression/Operand'); import Clock = require('../../src/Clock'); +import RecurringSession = require('../../src/goal/RecurringSession'); +import ExpressionFactory = require('../../src/condition/factory/ExpressionFactory'); -describe("Build a goal", function () { - var goal:Goal; +describe('Goal Test', () => { + var aGoalID:string = "5d34ae6e-e9ca-4352-9a67-3fdf205cce26"; + var aGoalName:string = "goal 1"; + var aBadgeID:string = 'badge 1'; - it("should have given name", () => { - goal = new Goal("aName", null, null, 0, null); - assert.equal(goal.getName(), "aName"); - }); -}); + var now:moment.Moment = moment('2015-08-26T00:00:00'); + var startDate:moment.Moment = moment("2015-08-17T:00:00:00"); + var endDate:moment.Moment = moment("2015-09-17T:23:59:59"); -describe("Add a condition to a goal", () => { - var goal:Goal; + var aRecurringSession:RecurringSession = new RecurringSession('week'); - beforeEach(() => { - goal = new Goal("aGoal", null, null, 0, null); - }); + var goal:Goal = new Goal(aGoalID, aGoalName, aBadgeID, now, endDate, aRecurringSession); - it('should have its own uuid', () => { - chai.expect(goal.hasUUID(goal.getUUID())).to.be.true; - }); + describe("Build a goal", function () { - it('should be possible to add an expression', () => { - var comparison:GoalExpression = new GoalExpression(new Operand("Température", true), 'inf', new Operand('10', false), 'desc'); - var expression:OverallGoalCondition = new OverallGoalCondition(null, comparison, 0, moment(new Date(Date.UTC(2000, 10, 10)).getTime()), moment(new Date(Clock.getNow()).getTime()), moment(new Date(Date.UTC(2000, 10, 15)).getTime())); - chai.expect(goal.addCondition(expression)).not.to.throw; - }); + it("should have given name", () => { + assert.equal(goal.getName(), aGoalName); + }); - it("should return the proper json", () => { - var expected:any = { - id: goal.getUUID(), - name: goal.getName(), - timeBox: { - startDate: goal.getStartDate(), - endDate: goal.getEndDate() - }, - duration: 'month', - conditions: goal.getConditions().getDataInJSON(), - badgeID: goal.getBadgeID() - } - - var actual = goal.getDataInJSON(); - - chai.expect(expected).to.be.eqls(actual); + it("should have given id", () => { + assert.equal(goal.getUUID(), aGoalID); + }); + + it("should have given badgeID", () => { + assert.equal(goal.getBadgeID(), aBadgeID); + }); + + it("should have given beginningOfValidityPeriod", () => { + assert.equal(goal.getBeginningOfValidityPeriod().toISOString(), now.toISOString()); + }); + + it("should have given endOfValidityPeriod", () => { + assert.equal(goal.getEndOfValidityPeriod().toISOString(), endDate.toISOString()); + }); }); - it("should call conditionsList evaluate on evaluate call", () => { - var goalStubObj = sinon.stub(goal.getConditions(), "evaluate"); - goalStubObj.onFirstCall().returns(true); - goal.evaluate({'a': null, 'b': null}); - chai.assert(goalStubObj.calledOnce); + describe("Add a condition to a goal", () => { + it('should be possible to add an expression', () => { + var aSymbolicName:string = 'Temp_cli'; + + var expression:GoalExpression; + var expressionDescription:any = { + valueLeft: { + value: aSymbolicName, + symbolicName: true + }, + valueRight: { + value: "15", + symbolicName: false + }, + comparison: ">" + }; + + var expressionFactory:ExpressionFactory = new ExpressionFactory(); + + var aConditionID = "id1"; + var aConditionDescription = "a desc"; + var aThresholdRate = 80; + + expression = expressionFactory.createExpression(expressionDescription); + var condition = new OverallGoalCondition(aConditionID, aConditionDescription, expression, aThresholdRate, null); + + goal.addCondition(condition); + + chai.expect(goal.getConditions().length).to.be.eql(1); + }); + + it("should return the proper json", () => { + var expected:any = { + id: goal.getUUID(), + name: goal.getName(), + timeBox: { + startDate: goal.getBeginningOfValidityPeriod(), + endDate: goal.getEndOfValidityPeriod() + }, + duration: 'month', + conditions: goal.getDataOfConditionsInJSON(), + badgeID: goal.getBadgeID() + }; + + var actual = goal.getDataInJSON(); + + chai.expect(expected).to.be.eqls(actual); + }); + }); }); diff --git a/backend/tests/integration/ChallengeBuildingTest.ts b/backend/tests/integration/ChallengeBuildingTest.ts index fbcadec..96b73f8 100644 --- a/backend/tests/integration/ChallengeBuildingTest.ts +++ b/backend/tests/integration/ChallengeBuildingTest.ts @@ -34,6 +34,8 @@ import DashboardRouter = require('../../src/api/DashboardRouter'); describe('UserChallenge integration test', () => { + /* + FIXME // Important ! Allow us to set time DashboardRouter.DEMO = true; @@ -86,4 +88,5 @@ describe('UserChallenge integration test', () => { var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-07T23:59:59+02:00")); chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(startDate.toISOString()); }); + */ }); \ No newline at end of file From b0a5cb0211a26872c18e5ea23b3713a5ddb98216 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 1 Sep 2015 09:57:53 +0200 Subject: [PATCH 15/28] Push "project" Yep. Second worst commit eva #Kalitaat --- backend/Gruntfile.js | 4 +- backend/db.json | 38 +++--- .../condition/expression/GoalExpression.ts | 4 +- .../src/condition/factory/ConditionFactory.ts | 10 +- backend/src/goal/GoalFactory.ts | 6 +- backend/src/user/Entity.ts | 2 +- backend/src/user/Team.ts | 111 +++++++++++++++++- backend/src/user/TeamFactory.ts | 3 +- backend/src/user/User.ts | 12 +- backend/tests/TeamTest.ts | 34 +++--- backend/tests/UserTest.ts | 52 ++------ backend/tests/condition/ConditionTest.ts | 4 +- .../condition/factory/ConditionFactoryTest.ts | 4 +- .../tests/database/GoalConditionStoreTest.ts | 3 +- backend/tests/database/UserStoreTest.ts | 2 +- backend/tests/filter/FilterTest.ts | 69 +++++------ backend/tests/goal/GoalFactoryTest.ts | 16 ++- backend/tests/goal/GoalTest.ts | 2 +- 18 files changed, 216 insertions(+), 160 deletions(-) diff --git a/backend/Gruntfile.js b/backend/Gruntfile.js index b15ef83..d9a5bd9 100644 --- a/backend/Gruntfile.js +++ b/backend/Gruntfile.js @@ -119,8 +119,8 @@ module.exports = function (grunt) { grunt.registerTask('build', function () { grunt.task.run(['clean:build','clean:test']); - // grunt.task.run(['typescript:build', 'typescript:test']); - grunt.task.run(['typescript:build']); + grunt.task.run(['typescript:build', 'typescript:test']); + // grunt.task.run(['typescript:build']); }); grunt.registerTask('develop', function() { diff --git a/backend/db.json b/backend/db.json index c85ad27..abbce46 100644 --- a/backend/db.json +++ b/backend/db.json @@ -3,33 +3,32 @@ { "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", "name": "Clim", - "timeBox": { - "startDate": "2015-07-01T06:58:42.000Z", - "endDate": "2015-09-30T13:01:24.000Z" + "validityPeriod": { + "start": "2015-07-01T06:58:42.000Z", + "end": "2015-09-30T13:01:24.000Z" }, - "duration": "week", + "recurringPeriod": "week", "conditions": [ { "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", "expression": { "valueLeft": { "value": "TMP_CLI", - "sensor": true + "symbolicName": true }, "valueRight": { "value": "15", - "sensor": false + "symbolicName": false }, "comparison": ">", "description": "a desc", "periodOfTime": "-2208474000000" }, "threshold": 25, - "startDate": "2015-08-02T22:00:00.000Z", - "dateOfCreation": "2015-08-09T22:00:00.000Z", - "endDate": "2015-08-14T21:59:59.999Z", - "percentageAchieved": 100, - "percentageOfTimeElapsed": 100, + "referencePeriod" :{ + "numberOfUnitToSubtract":1, + "unitToSubtract":"week" + }, "filter": { "dayOfWeekFilter": "all", "periodOfDayFilter": [ @@ -45,32 +44,27 @@ { "id": "9bddaf87-5065-4df7-920a-d1d249c9171d", "name": "Obj1", - "timeBox": { - "startDate": "2015-08-04T12:25:57.787Z", - "endDate": "2015-08-31T12:25:57.787Z" + "validityPeriod": { + "start": "2015-08-04T12:25:57.787Z", + "end": "2015-08-31T12:25:57.787Z" }, - "duration": "week", + "recurringPeriod": "week", "conditions": [ { "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", "expression": { "valueLeft": { "value": "TMP_CLI", - "sensor": true + "symbolicName": true }, "valueRight": { "value": 1, - "sensor": false + "symbolicName": false }, "comparison": ">", "description": "tmp_cli > 1" }, "threshold": 100, - "startDate": "2015-08-04T12:25:57.787Z", - "dateOfCreation": "2015-08-07T07:56:06.107Z", - "endDate": "2015-08-31T12:25:57.787Z", - "percentageAchieved": 0, - "percentageOfTimeElapsed": 0, "filter": { "dayOfWeekFilter": "working-week", "periodOfDayFilter": [ diff --git a/backend/src/condition/expression/GoalExpression.ts b/backend/src/condition/expression/GoalExpression.ts index cbb7307..83749bd 100644 --- a/backend/src/condition/expression/GoalExpression.ts +++ b/backend/src/condition/expression/GoalExpression.ts @@ -113,11 +113,11 @@ class GoalExpression { return { valueLeft: { value: this.leftOperand.getStringDescription(), - sensor: this.leftOperand.hasToBeDefined() + symbolicName: this.leftOperand.hasToBeDefined() }, valueRight: { value: this.rightOperand.getStringDescription(), - sensor: this.rightOperand.hasToBeDefined() + symbolicName: this.rightOperand.hasToBeDefined() }, comparison: this.comparator.getTypeOfComparison() }; diff --git a/backend/src/condition/factory/ConditionFactory.ts b/backend/src/condition/factory/ConditionFactory.ts index 9c7e061..fe2f123 100644 --- a/backend/src/condition/factory/ConditionFactory.ts +++ b/backend/src/condition/factory/ConditionFactory.ts @@ -17,12 +17,12 @@ import ReferencePeriod = require('../ReferencePeriod'); class ConditionFactory { private expressionFactory:ExpressionFactory = new ExpressionFactory(); - public createCondition(data:any, goalTimeBox:any):Condition { + public createCondition(data:any):Condition { var type:string = data.type; var expression = null; switch (type) { case 'overall': - expression = this.createOverall(data, goalTimeBox); + expression = this.createOverall(data); break; case 'comparison': expression = this.createComparison(data); @@ -34,15 +34,11 @@ class ConditionFactory { return expression; } - public createOverall(data:any, goaltimeBox:any):Condition { + public createOverall(data:any):Condition { - data.expression.timeBox = goaltimeBox; var goalExpression:GoalExpression = this.expressionFactory.createExpression(data.expression); - var startDateOfValidityPeriod:moment.Moment = goaltimeBox.startDate; - var endDateOfValidityPeriod:moment.Moment = goaltimeBox.endDate; - var threshold:number = data.threshold; var dayOfWeekFilterDesc:string = data.filter.dayOfWeekFilter; diff --git a/backend/src/goal/GoalFactory.ts b/backend/src/goal/GoalFactory.ts index 8ddf912..8fbc219 100644 --- a/backend/src/goal/GoalFactory.ts +++ b/backend/src/goal/GoalFactory.ts @@ -29,6 +29,8 @@ class GoalFactory { public createGoal(data:any):Goal { + this.checkData(data); + var goalID = (data.id == null) ? UUID.v4() : data.id; var goalName:string = data.name; var badge:string = data.badgeID; @@ -43,7 +45,7 @@ class GoalFactory { var goalConditions:any[] = data.conditions; for (var i = 0; i < goalConditions.length; i++) { - var currentExpression = this.conditionFactory.createCondition(goalConditions[i], data.timeBox); + var currentExpression = this.conditionFactory.createCondition(goalConditions[i]); newGoal.addCondition(currentExpression); } // console.log("Creation de l'objectif", goalName, "valide du", startDateOfValidityPeriod, "au", endDateOfValidityPeriod, "avec le badge", newGoal.getBadgeID()); @@ -65,7 +67,7 @@ class GoalFactory { } if (data.recurringPeriod == null) { - throw new BadArgumentException('Can not create given goal because array "recurringPeriod" is null'); + throw new BadArgumentException('Can not create given goal because field "recurringPeriod" is null'); } if (data.validityPeriod == null) { diff --git a/backend/src/user/Entity.ts b/backend/src/user/Entity.ts index ab7483c..bc3dc46 100644 --- a/backend/src/user/Entity.ts +++ b/backend/src/user/Entity.ts @@ -11,7 +11,7 @@ class Entity { private id; private name:string; - private currentChallenges:string[] = []; + protected currentChallenges:string[] = []; private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; constructor(id:string, name:string, currentChallenges:string[], diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 08161ff..82a9da9 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -4,18 +4,101 @@ import BadArgumentException = require('../exceptions/BadArgumentException'); import Entity = require('./Entity'); import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarnedMap'); import User = require('./User'); +import Goal = require('../goal/Goal'); +import TeamChallengeFactory = require('../challenge/TeamChallengeFactory'); + +class Team { + private id; + private name:string; + private currentChallenges:string[] = []; + private badgesMap:BadgeIDsToNumberOfTimesEarnedMap = {}; -class Team extends Entity { private members:User[] = []; private leader:User; + private challengeFactory:TeamChallengeFactory; + constructor(id:string, name:string, leader:User, members:User[], - currentChallenges:string[], badgesMap:BadgeIDsToNumberOfTimesEarnedMap) { + currentChallenges:string[], badgesMap:BadgeIDsToNumberOfTimesEarnedMap, teamChallengeFactory:TeamChallengeFactory) { - super(id, name, currentChallenges, badgesMap); + this.id = id; + this.name = name; + this.badgesMap = badgesMap; + this.currentChallenges = currentChallenges; this.leader = leader; this.members = members; + + this.challengeFactory = teamChallengeFactory; + } + + getUUID() { + return this.id; + } + + hasUUID(aUUID:string):boolean { + return this.id === aUUID; + } + + setUUID(aUUID:string):void { + this.id = aUUID; + } + + getName():string { + return this.name; + } + + hasName(name:string):boolean { + return this.getName() === name; + } + + setName(name:string):void { + this.name = name; + } + + getCurrentChallenges():string [] { + return this.currentChallenges; + } + + addChallenge(goal:Goal, now:moment.Moment):TeamChallengeFactory { + var newChallenge = this.challengeFactory.createTeamChallenge(this, goal, now); + + /*FIXME + // Check if we try + if (newChallenge.getEndDate().isAfter(goal.getEndOfValidityPeriod())) { + return null; + } + + this.currentChallenges.push(newChallenge.getId()); + return newChallenge; + */ + + return null; + } + + deleteChallenge(challengeID:string):void { + + var challengeIndex:number = this.getChallengeByID(challengeID); + if (challengeIndex == -1) { + throw new BadArgumentException('Can not find given challenge ID'); + } + else { + this.currentChallenges.splice(challengeIndex, 1); + } + + console.log("UserChallenge deleted ! Current challenges:", this.currentChallenges); + } + + private getChallengeByID(challengeID:string):number { + var result:number = -1; + + for (var currentChallengeIndex = 0; currentChallengeIndex < this.currentChallenges.length; currentChallengeIndex++) { + if (this.currentChallenges[currentChallengeIndex] === challengeID) { + result = currentChallengeIndex; + } + } + + return result; } getLeader():User { @@ -55,6 +138,28 @@ class Team extends Entity { return result; } + + getBadgesID():string[] { + return Object.keys(this.badgesMap); + } + + public getDataInJSON():any { + var membersIDs:any[] =[]; + for(var memberIndex in this.members) { + var currentMember = this.members[memberIndex]; + var currentMemberID = currentMember.getUUID(); + membersIDs.push(currentMemberID); + } + + return { + id: this.id, + name: this.name, + leader: this.leader.getUUID(), + members: membersIDs, + currentChallenges: this.currentChallenges, + finishedBadgesMap: this.badgesMap + } + } } export = Team; \ No newline at end of file diff --git a/backend/src/user/TeamFactory.ts b/backend/src/user/TeamFactory.ts index 4a4fbef..f26d3e7 100644 --- a/backend/src/user/TeamFactory.ts +++ b/backend/src/user/TeamFactory.ts @@ -49,7 +49,8 @@ class TeamFactory { var leaderID:string = data.leader; var leader = userRepository.getUser(leaderID); - var team:Team = new Team(teamID, teamName, leader, members, currentChallenges, finishedBadgesMap); + //TODO FIX NULL + var team:Team = new Team(teamID, teamName, leader, members, currentChallenges, finishedBadgesMap, null); return team; } } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index 5df4771..8fc8332 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -27,15 +27,7 @@ class User { constructor(name:string, mapSymbolicNameToSensor:any, currentChallenges:string[], finishedBadgesMap:BadgeIDsToNumberOfTimesEarnedMap, challengeFactory:ChallengeFactory, id = null) { - if (name == null) { - throw new BadArgumentException('Can not build user because given name is null'); - } - - if (mapSymbolicNameToSensor == null) { - throw new BadArgumentException('Can not build user ' + name + ' because given map of symbolic name to sensor is null'); - } - - this.id = (id) ? id : uuid.v4(); + this.id = id; this.name = name; this.mapSymbolicNameToSensor = mapSymbolicNameToSensor; @@ -71,7 +63,7 @@ class User { return this.badgesMap; } - public addBadge(badgeID:string) { + addBadge(badgeID:string) { if (!badgeID) { throw new BadArgumentException('Can not add given badge to user' + this.getName() + '. Badge given is null'); } diff --git a/backend/tests/TeamTest.ts b/backend/tests/TeamTest.ts index 8a1e6c7..6961a00 100644 --- a/backend/tests/TeamTest.ts +++ b/backend/tests/TeamTest.ts @@ -23,16 +23,15 @@ describe("Test a team", function () { var members:User[] = []; var team:Team; + aMember = new User('Gégé', aMapSymoblicNameToSensor, [], [], null); + anotherMember = new User('Dédé', anotherMapSymoblicNameToSensor, [], [], null); + members = [aMember, anotherMember]; - beforeEach(() => { - aMember = new User('Gégé', aMapSymoblicNameToSensor, [], [], null); - anotherMember = new User('Dédé', anotherMapSymoblicNameToSensor, [], [], null); - members = [aMember, anotherMember]; - team = new Team("id", "Croquette", aMember, members, [], null); - }); describe('Check its composition', () => { - + beforeEach(() => { + team = new Team("id", "Croquette", aMember, members, [], null, null); + }); it('should have proper leader', () => { chai.expect(team.hasLeader(aMember.getUUID())).to.be.true; }); @@ -44,24 +43,23 @@ describe("Test a team", function () { }); describe('Check add method', () => { - it('should have a challenge when it was previously added', () => { - chai.expect(team.getCurrentChallenges()).to.be.eqls([]); - - var aChallengeID = 'aChallengeID'; - team.addChallenge(aChallengeID); + beforeEach(() => { + team = new Team("id", "Croquette", aMember, members, [], null, null); + }); - chai.expect(team.getCurrentChallenges()).to.be.eqls([aChallengeID]); + it('should have no challenge by default', () => { + chai.expect(team.getCurrentChallenges()).to.be.eqls([]); }); - it('should have added a challenge to its members when it was previously added', () => { - chai.expect(aMember.getCurrentChallenges()).to.be.eqls([]); - chai.expect(anotherMember.getCurrentChallenges()).to.be.eqls([]); + it('should have a challenge when it was previously added', () => { + /* FIXME + chai.expect(team.getCurrentChallenges()).to.be.eqls([]); var aChallengeID = 'aChallengeID'; team.addChallenge(aChallengeID); - chai.expect(aMember.getCurrentChallenges()).to.be.eqls([aChallengeID]); - chai.expect(anotherMember.getCurrentChallenges()).to.be.eqls([aChallengeID]); + chai.expect(team.getCurrentChallenges()).to.be.eqls([aChallengeID]); + */ }); }); }); diff --git a/backend/tests/UserTest.ts b/backend/tests/UserTest.ts index 3b71f73..6f412f2 100644 --- a/backend/tests/UserTest.ts +++ b/backend/tests/UserTest.ts @@ -9,51 +9,21 @@ var assert = chai.assert; import User = require('../src/user/User'); -describe("Build a User", function () { - var user:User; +describe('User test', () => { - it("should have given name", () => { - user = new User("aName", null, [], null, null); - assert.equal(user.getName(), "aName"); - }); -}); + var mapSymbolicNameToSensor:any = { + 'TMP_CLI': 'AC_554' + }; -describe("Add a goal", function () { - var user:User; + describe("Build a User", function () { + var user:User; - beforeEach(() => { - user = new User("aName", null, [], null, null); + it("should have given name", () => { + user = new User("aName", mapSymbolicNameToSensor, [], null, null); + assert.equal(user.getName(), "aName"); + }); }); - it("should throw an error if given goal is null", () => { - chai.expect(() => user.addChallenge(null, null)).to.throw(Error); - }); + //TODO TESTS - it("should add the goal to the user's description", () => { - /* - user.addGoalByDescription(new Goal("a", "a", 0)); - chai.expect(user.getGoals().length).to.be.equals(1); - */ - }); - it("should add the given goal to the user's description", () => { - /* - var goal:Goal = new Goal("a", "a", 0); - user.addGoalByDescription(goal); - chai.expect(user.getGoals().pop()).to.be.equals(goal); - */ - }); -}); - -describe("evaluate a goal", function () { - var user:User; - - beforeEach(() => { - user = new User("aName", null, [], null, null); - }); - - it("should return false if given goalName doesn't exist", () => { - // var goal:Goal = new Goal("a", "inf", 10); - //user.addGoalByDescription(goal); - //assert.isFalse(user.evaluateGoal("aNameThatDoesntExist", 0)); - }); }); diff --git a/backend/tests/condition/ConditionTest.ts b/backend/tests/condition/ConditionTest.ts index 5ef27fe..d34f906 100644 --- a/backend/tests/condition/ConditionTest.ts +++ b/backend/tests/condition/ConditionTest.ts @@ -79,11 +79,11 @@ describe('Test Condition', () => { var expected:any = { valueLeft: { value: aSymbolicName, - sensor: true + symbolicName: true }, valueRight: { value: aValue, - sensor: false + symbolicName: false }, comparison: aComparison }; diff --git a/backend/tests/condition/factory/ConditionFactoryTest.ts b/backend/tests/condition/factory/ConditionFactoryTest.ts index d8c9ded..acc6ce1 100644 --- a/backend/tests/condition/factory/ConditionFactoryTest.ts +++ b/backend/tests/condition/factory/ConditionFactoryTest.ts @@ -20,8 +20,8 @@ describe("ConditionFactory test", () => { jsonExpression.type = 'number'; jsonExpression.description = 'description blabla ..'; - jsonExpression.valueLeft = {'value' : 'TEMP_CLI', 'sensor':true}; - jsonExpression.valueRight = {'value' : '15', 'sensor':false}; + jsonExpression.valueLeft = {'value' : 'TEMP_CLI', 'symbolicName':true}; + jsonExpression.valueRight = {'value' : '15', 'symbolicName':false}; expression = factory.createExpression(jsonExpression); diff --git a/backend/tests/database/GoalConditionStoreTest.ts b/backend/tests/database/GoalConditionStoreTest.ts index e7d3fc1..d8eb00c 100644 --- a/backend/tests/database/GoalConditionStoreTest.ts +++ b/backend/tests/database/GoalConditionStoreTest.ts @@ -36,8 +36,7 @@ describe('Test store GoalCondition class', () => { value: rightOperand.getStringDescription(), symbolicName: rightOperand.hasToBeDefined() }, - comparison: typeOfComparison, - description: description + comparison: typeOfComparison }; it('should return the proper json object', () => { diff --git a/backend/tests/database/UserStoreTest.ts b/backend/tests/database/UserStoreTest.ts index c1e7ad7..9f75c12 100644 --- a/backend/tests/database/UserStoreTest.ts +++ b/backend/tests/database/UserStoreTest.ts @@ -36,7 +36,7 @@ describe('Test store user class', () => { }); describe('build with its own description', () => { - var userClone:User = new User(expected.name,expected.mapSymbolicNameToSensor,expected.currentChallenges, expected.finishedBadgesMap, expected.id); + var userClone:User = new User(expected.name,expected.mapSymbolicNameToSensor,expected.currentChallenges, expected.finishedBadgesMap,null, expected.id); it('should have the same name', () => { chai.expect(userClone.getName()).to.be.eq(aName); diff --git a/backend/tests/filter/FilterTest.ts b/backend/tests/filter/FilterTest.ts index bfe2044..0832c92 100644 --- a/backend/tests/filter/FilterTest.ts +++ b/backend/tests/filter/FilterTest.ts @@ -16,32 +16,30 @@ describe('FilterTest test', () => { var jsonValuesInMorningAndAfternoon:any = { - 'TMP_CLI': { - 'values': [ - {"date": "1436522344000", "value": "28"}, // 10/7/2015 11:59:04 GMT+2:00 DST - {"date": "1436522374000", "value": "29"}, // 10/7/2015 11:59:34 GMT+2:00 DST - {"date": "1436522404000", "value": "17"}, // 10/7/2015 12:00:04 GMT+2:00 DST - {"date": "1436522434000", "value": "30"}, // 10/7/2015 12:00:34 GMT+2:00 DST - {"date": "1436522464000", "value": "25"}, // 10/7/2015 12:01:04 GMT+2:00 DST - {"date": "1436522494000", "value": "21"}, // 10/7/2015 12:01:34 GMT+2:00 DST - {"date": "1438608351000", "value": "3"} // 3/8/2015 15:25:51 - ] - } + 'TMP_CLI': [ + {"date": "1436522344000", "value": "28"}, // 10/7/2015 11:59:04 GMT+2:00 DST + {"date": "1436522374000", "value": "29"}, // 10/7/2015 11:59:34 GMT+2:00 DST + {"date": "1436522404000", "value": "17"}, // 10/7/2015 12:00:04 GMT+2:00 DST + {"date": "1436522434000", "value": "30"}, // 10/7/2015 12:00:34 GMT+2:00 DST + {"date": "1436522464000", "value": "25"}, // 10/7/2015 12:01:04 GMT+2:00 DST + {"date": "1436522494000", "value": "21"}, // 10/7/2015 12:01:34 GMT+2:00 DST + {"date": "1438608351000", "value": "3"} // 3/8/2015 15:25:51 + ] + }; var jsonValuesInAfternoon:any = { - 'TMP_CLI': { - 'values': [ - {"date": "1436446840000", "value": "28"}, // 9/7/2015 15:00:40 GMT+2:00 DST - {"date": "1436446870000", "value": "26"}, // 9/7/2015 15:01:10 GMT+2:00 DST - {"date": "1436446900000", "value": "28"}, // 9/7/2015 15:01:40 GMT+2:00 DST - {"date": "1436446930000", "value": "28"}, // 9/7/2015 15:02:10 GMT+2:00 DST - {"date": "1436446960000", "value": "27"}, // 9/7/2015 15:02:40 GMT+2:00 DST - {"date": "1436446990000", "value": "28"}, // 9/7/2015 15:03:10 GMT+2:00 DST - {"date": "1436447020000", "value": "28"} // 9/7/2015 15:03:40 GMT+2:00 DST - ] - } + 'TMP_CLI': [ + {"date": "1436446840000", "value": "28"}, // 9/7/2015 15:00:40 GMT+2:00 DST + {"date": "1436446870000", "value": "26"}, // 9/7/2015 15:01:10 GMT+2:00 DST + {"date": "1436446900000", "value": "28"}, // 9/7/2015 15:01:40 GMT+2:00 DST + {"date": "1436446930000", "value": "28"}, // 9/7/2015 15:02:10 GMT+2:00 DST + {"date": "1436446960000", "value": "27"}, // 9/7/2015 15:02:40 GMT+2:00 DST + {"date": "1436446990000", "value": "28"}, // 9/7/2015 15:03:10 GMT+2:00 DST + {"date": "1436447020000", "value": "28"} // 9/7/2015 15:03:40 GMT+2:00 DST + ] + }; describe('apply method', () => { @@ -62,15 +60,14 @@ describe('FilterTest test', () => { var expected:any = { - 'TMP_CLI': { - 'values': [ - {"date": "1436522404000", "value": "17"}, // 10/7/2015 12:00:04 GMT+2:00 DST - {"date": "1436522434000", "value": "30"}, // 10/7/2015 12:00:34 GMT+2:00 DST - {"date": "1436522464000", "value": "25"}, // 10/7/2015 12:01:04 GMT+2:00 DST - {"date": "1436522494000", "value": "21"}, // 10/7/2015 12:01:34 GMT+2:00 DST - {"date": "1438608351000", "value": "3"} // 3/8/2015 15:25:51 - ] - } + 'TMP_CLI': [ + {"date": "1436522404000", "value": "17"}, // 10/7/2015 12:00:04 GMT+2:00 DST + {"date": "1436522434000", "value": "30"}, // 10/7/2015 12:00:34 GMT+2:00 DST + {"date": "1436522464000", "value": "25"}, // 10/7/2015 12:01:04 GMT+2:00 DST + {"date": "1436522494000", "value": "21"}, // 10/7/2015 12:01:34 GMT+2:00 DST + {"date": "1438608351000", "value": "3"} // 3/8/2015 15:25:51 + ] + }; console.log("RESULT", JSON.stringify(result), "VS", JSON.stringify(expected)); @@ -94,9 +91,8 @@ describe('FilterTest test', () => { var result:any[] = filter.apply(jsonValuesInMorningAndAfternoon); var expected:any = { - 'TMP_CLI': { - 'values': [] - } + 'TMP_CLI': [] + }; chai.expect(result).to.be.eqls(expected); @@ -107,9 +103,8 @@ describe('FilterTest test', () => { var result:any[] = filter.apply(jsonValuesInMorningAndAfternoon); var expected:any = { - 'TMP_CLI': { - 'values': [] - } + 'TMP_CLI': [] + }; chai.expect(result).to.be.eqls(expected); diff --git a/backend/tests/goal/GoalFactoryTest.ts b/backend/tests/goal/GoalFactoryTest.ts index 32b9598..af16054 100644 --- a/backend/tests/goal/GoalFactoryTest.ts +++ b/backend/tests/goal/GoalFactoryTest.ts @@ -18,14 +18,18 @@ describe("GoalFactory test", () => { var start = Clock.getMomentFromString("2000-01-01T00:00:00"); var end = Clock.getMomentFromString("2000-08-01T00:00:00"); + console.log("START ?", start.toISOString()); + beforeEach(() => { var jsonDefinition:any = {}; jsonDefinition.name = "Clim"; + jsonDefinition.badgeID = "badgeID"; + jsonDefinition.recurringPeriod = "week"; - var timeBox:any = {}; - timeBox.startDate = start.toISOString(); - timeBox.endDate = end.toISOString(); - jsonDefinition.timeBox = timeBox; + var validityPeriod:any = {}; + validityPeriod.start = start.toISOString(); + validityPeriod.end = end.toISOString(); + jsonDefinition.validityPeriod = validityPeriod; jsonDefinition.duration = 'day'; @@ -33,8 +37,8 @@ describe("GoalFactory test", () => { jsonExpression.comparison = '<'; jsonExpression.type = 'number'; jsonExpression.description = 'description blabla ..'; - jsonExpression.valueLeft = {'value': 'TEMP_CLI', 'sensor': true}; - jsonExpression.valueRight = {'value': '15', 'sensor': false}; + jsonExpression.valueLeft = {'value': 'TEMP_CLI', 'symbolicName': true}; + jsonExpression.valueRight = {'value': '15', 'symbolicName': false}; var aJsonCondition:any = {}; aJsonCondition.type = 'overall'; diff --git a/backend/tests/goal/GoalTest.ts b/backend/tests/goal/GoalTest.ts index a082d3d..5cdec54 100644 --- a/backend/tests/goal/GoalTest.ts +++ b/backend/tests/goal/GoalTest.ts @@ -96,7 +96,7 @@ describe('Goal Test', () => { startDate: goal.getBeginningOfValidityPeriod(), endDate: goal.getEndOfValidityPeriod() }, - duration: 'month', + duration: aRecurringSession.getDescription(), conditions: goal.getDataOfConditionsInJSON(), badgeID: goal.getBadgeID() }; From b7b860a67c2b6d104add592c7d4f890690f1154e Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Tue, 1 Sep 2015 16:36:50 +0200 Subject: [PATCH 16/28] Add integration tests --- backend/Gruntfile.js | 36 ++- backend/db.json | 26 +-- backend/db_test.json | 139 ++++++++++++ backend/package.json | 1 + backend/src/Backend.ts | 76 ++----- backend/src/Context.ts | 205 +++++++++++++++++- backend/src/JSONSerializer.ts | 16 +- backend/src/Server.ts | 4 +- backend/src/StoringHandler.ts | 102 ++------- backend/src/api/DashboardRouter.ts | 159 +++++++++----- backend/src/api/LoginRouter.ts | 49 +++-- backend/src/challenge/UserChallenge.ts | 133 +++++------- backend/src/challenge/UserChallengeFactory.ts | 3 +- .../src/challenge/UserChallengeRepository.ts | 6 +- backend/src/condition/AverageOnValue.ts | 2 +- backend/src/condition/OverallGoalCondition.ts | 8 +- .../src/condition/factory/ConditionFactory.ts | 12 +- backend/src/context/DemoContext.ts | 56 +---- backend/src/context/TestContext.ts | 3 + .../src/exceptions/BadArgumentException.ts | 4 + backend/src/exceptions/BadRequestException.ts | 4 + backend/src/goal/Goal.ts | 24 +- backend/src/goal/GoalRepository.ts | 2 +- backend/src/user/User.ts | 4 + backend/stub_values.json | 16 +- backend/stub_values_test.json | 12 + backend/tests/UserTest.ts | 1 + .../condition/OverallGoalConditionTest.ts | 8 +- backend/tests/db_test.json | 139 ++++++++++++ backend/tests/goal/GoalTest.ts | 11 +- .../integration/ChallengeBuildingTest.ts | 92 -------- .../integration/ChallengeIntegrationTest.ts | 114 ++++++++++ 32 files changed, 939 insertions(+), 528 deletions(-) create mode 100644 backend/db_test.json create mode 100644 backend/src/context/TestContext.ts create mode 100644 backend/stub_values_test.json create mode 100644 backend/tests/db_test.json delete mode 100644 backend/tests/integration/ChallengeBuildingTest.ts create mode 100644 backend/tests/integration/ChallengeIntegrationTest.ts diff --git a/backend/Gruntfile.js b/backend/Gruntfile.js index d9a5bd9..39bbb47 100644 --- a/backend/Gruntfile.js +++ b/backend/Gruntfile.js @@ -5,6 +5,7 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-express-server'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); grunt.loadNpmTasks('grunt-typescript'); grunt.loadNpmTasks('grunt-mocha-test'); @@ -56,12 +57,12 @@ module.exports = function (grunt) { }, build: { options: { - script: 'build/Backend.js' + script: 'build/Context.js' } }, dist: { options: { - script: 'dist/Backend.js', + script: 'dist/Context.js', node_env: 'production' } } @@ -73,7 +74,7 @@ module.exports = function (grunt) { // --------------------------------------------- watch: { express: { - files: [ 'build/Backend.js' ], + files: [ 'build/Context.js' ], tasks: [ 'express:build' ], options: { spawn: false @@ -109,8 +110,29 @@ module.exports = function (grunt) { dist: ['dist/'], test: ['buildTests/'], all:['src/**/*.js', 'src/**/*js.map','tests/**/*.js', 'tests/**/*js.map'] - } + }, +// --------------------------------------------- + + // --------------------------------------------- +// copy task +// --------------------------------------------- + copy: { + testFiles: { + files: [{ + expand: true, + src: ['db_test.json', 'stub_values_test.json', 'db.json', 'stub_values.json'], + dest: 'buildTests/' + }] + }, + prodFiles: { + files: [{ + expand: true, + src: ['db_test.json', 'stub_values_test.json', 'db.json', 'stub_values.json'], + dest: 'build/' + }] + } + } }); // register tasks @@ -119,8 +141,8 @@ module.exports = function (grunt) { grunt.registerTask('build', function () { grunt.task.run(['clean:build','clean:test']); - grunt.task.run(['typescript:build', 'typescript:test']); - // grunt.task.run(['typescript:build']); + grunt.task.run(['typescript:build','typescript:test', 'copy:prodFiles']); + //grunt.task.run(['typescript:build']); }); grunt.registerTask('develop', function() { @@ -136,7 +158,7 @@ module.exports = function (grunt) { grunt.registerTask('test', function() { grunt.task.run(['clean:test']); - grunt.task.run(['typescript:test', 'mochaTest:test']); + grunt.task.run(['copy:testFiles','typescript:test', 'mochaTest:test']); }); } \ No newline at end of file diff --git a/backend/db.json b/backend/db.json index abbce46..b45b2d5 100644 --- a/backend/db.json +++ b/backend/db.json @@ -11,6 +11,7 @@ "conditions": [ { "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", + "description":"a desc", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -21,7 +22,6 @@ "symbolicName": false }, "comparison": ">", - "description": "a desc", "periodOfTime": "-2208474000000" }, "threshold": 25, @@ -52,6 +52,7 @@ "conditions": [ { "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", + "description": "tmp_cli > 1", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -61,8 +62,7 @@ "value": 1, "symbolicName": false }, - "comparison": ">", - "description": "tmp_cli > 1" + "comparison": ">" }, "threshold": 100, "filter": { @@ -121,26 +121,6 @@ { "userChallenges": [ - { - "id": "af0947e9-bf85-4233-8d50-2de787bf6021", - "name": "Clim", - "timeProgress": 100, - "startDate": "2015-08-02T22:00:00.000Z", - "endDate": "2015-08-07T21:59:59.999Z", - "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", - "status": "SUCCESS" - }, - { - "id": "f3abd585-b5a2-43d2-bced-738d646921b8", - "name": "Clim", - "timeProgress": 0, - "startDate": "2015-08-09T22:00:00.000Z", - "endDate": "2015-08-14T21:59:59.999Z", - "goalID": "3221c575-85ca-447b-86f3-3a4ef39985dc", - "userID":"2cf91e02-a320-4766-aa9f-6efce3142d44", - "status": "WAIT" - } ], "teamChallenges":[] } diff --git a/backend/db_test.json b/backend/db_test.json new file mode 100644 index 0000000..0e640cf --- /dev/null +++ b/backend/db_test.json @@ -0,0 +1,139 @@ +{ + "definitions": [ + { + "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "name": "Clim", + "validityPeriod": { + "start": "2015-08-03T22:00:00.000Z", + "end": "2015-09-03T22:00:00.000Z" + }, + "recurringPeriod": "week", + "conditions": [ + { + "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": "15", + "symbolicName": false + }, + "comparison": ">", + "description": "a desc", + "periodOfTime": "-2208474000000" + }, + "threshold": 25, + "referencePeriod": { + "numberOfUnitToSubtract": 1, + "unitToSubtract": "week" + }, + "filter": { + "dayOfWeekFilter": "all", + "periodOfDayFilter": [ + "morning", + "afternoon" + ] + }, + "type": "comparison" + } + ], + "badgeID": "44bb8108-8830-4f43-abd1-3ef643303d92" + }, + { + "id": "9bddaf87-5065-4df7-920a-d1d249c9171d", + "name": "Obj1", + "validityPeriod": { + "start": "2015-08-04T12:25:57.787Z", + "end": "2015-08-31T12:25:57.787Z" + }, + "recurringPeriod": "week", + "conditions": [ + { + "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": 1, + "symbolicName": false + }, + "comparison": ">", + "description": "tmp_cli > 1" + }, + "threshold": 100, + "filter": { + "dayOfWeekFilter": "working-week", + "periodOfDayFilter": [ + "all" + ] + }, + "type": "overall" + } + ], + "badgeID": "fde68334-f515-4563-954b-ac91b4a42f88" + } + ], + "badges": [ + { + "id": "44bb8108-8830-4f43-abd1-3ef643303d92", + "name": "Un challenge de d\u00e9mo !", + "points": 100 + } + ], + "users": [ + { + "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "name": "Charlie", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + }, + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_443V" + } + }, + { + "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "name": "Gégé", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + }, + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_352" + } + } + ], + "teams": [ + { + "id": "28aa8108-8830-4f43-abd1-3ab643303d92", + "name": "croquette", + "members": [ + "2cf91e02-a320-4766-aa9f-6efce3142d44" + ], + "leader": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + } + } + ], + "challenges": { + "userChallenges": [ + + ], + "teamChallenges": [ + + ] + } +} \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index fe450fe..1343031 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,6 +15,7 @@ "chai": "^3.0.0", "grunt": "^0.4.5", "grunt-contrib-clean": "^0.6.0", + "grunt-contrib-copy": "^0.8.1", "grunt-contrib-watch": "^0.6.1", "grunt-express-server": "^0.5.1", "grunt-mocha-test": "^0.12.7", diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index d469163..d9c6505 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -7,48 +7,17 @@ import Server = require('./Server'); import DashboardRouter = require('./api/DashboardRouter'); import LoginRouter = require('./api/LoginRouter'); -import BadgeRepository = require('./badge/BadgeRepository'); -import BadgeFactory = require('./badge/BadgeFactory'); - -import GoalRepository = require('./goal/GoalRepository'); -import GoalFactory = require('./goal/GoalFactory'); - -import UserChallengeRepository = require('./challenge/UserChallengeRepository'); -import UserChallengeFactory = require('./challenge/UserChallengeFactory'); - -import UserRepository = require('./user/UserRepository'); -import UserFactory = require('./user/UserFactory'); -import User = require('./user/User'); - -import TeamRepository = require('./user/TeamRepository'); -import TeamFactory = require('./user/TeamFactory'); - -import Operand = require('./condition/expression/Operand'); -import GoalExpression = require('./condition/expression/GoalExpression'); -import OverallGoalCondition = require('./condition/OverallGoalCondition'); -import TimeBox = require('./TimeBox'); +import Context = require('./Context'); import StoringHandler = require('./StoringHandler'); import Middleware = require('./Middleware'); class Backend extends Server { - public badgeRepository:BadgeRepository; - public badgeFactory:BadgeFactory; - - public goalDefinitionRepository:GoalRepository; - public goalDefinitionFactory:GoalFactory; - - public challengeRepository:UserChallengeRepository; - public challengeFactory:UserChallengeFactory; + private context:Context; - public userRepository:UserRepository; - public userFactory:UserFactory; - - public teamRepository:TeamRepository; - public teamFactory:TeamFactory; - - private storingHandler:StoringHandler; + public static PATH_TO_DB:string = "./db.json"; + public static PATH_TO_STUB:string = "./stub_values.json"; /** * Constructor. @@ -57,26 +26,10 @@ class Backend extends Server { * @param {Array} arguments - Server's command line arguments. */ constructor(listeningPort:number, arguments:Array) { - this.userRepository = new UserRepository(); - - super(listeningPort, arguments, this.userRepository); - - this.badgeRepository = new BadgeRepository(); - this.badgeFactory = new BadgeFactory(); - - this.goalDefinitionRepository = new GoalRepository(this.badgeRepository); - this.goalDefinitionFactory = new GoalFactory(); - - this.challengeRepository = new UserChallengeRepository(); - this.challengeFactory = new UserChallengeFactory(); - - this.userRepository = new UserRepository(); - this.userFactory = new UserFactory(); + super(listeningPort, arguments); - this.teamRepository = new TeamRepository(); - this.teamFactory = new TeamFactory(); + this.context = new Context(Backend.PATH_TO_DB, Backend.PATH_TO_STUB); - this.storingHandler = new StoringHandler(this); this.loadData(); this.buildAPI(); @@ -90,8 +43,8 @@ class Backend extends Server { buildAPI() { var self = this; - this.app.use('/dashboard', (new DashboardRouter(self.challengeRepository, self.challengeFactory, self.goalDefinitionRepository, self.userRepository, self.teamRepository, self.badgeRepository, new Middleware())).getRouter()); - this.app.use('/login', (new LoginRouter(self.userRepository)).getRouter()); + this.app.use('/dashboard', (new DashboardRouter(self.context, new Middleware())).getRouter()); + this.app.use('/login', (new LoginRouter(self.context)).getRouter()); /* this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); @@ -99,8 +52,8 @@ class Backend extends Server { this.app.use("/challenges", (new GoalInstanceRouter(self.challengeRepository, self.challengeFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); */ - this.app.get('/test', function (req, res) { - self.storingHandler.save( + this.app.get('/save', function (req, res) { + self.context.saveData( function (result) { console.log(result.success); }, @@ -112,17 +65,14 @@ class Backend extends Server { } loadData():void { - var result = this.storingHandler.load(); - if (result.success) { - console.log(result.success); - } + var result = this.context.loadData(); } } export = Backend; /** - * Server's Backend listening port. + * Server's Context listening port. * * @property _BackendListeningPort * @type number @@ -131,7 +81,7 @@ export = Backend; var _BackendListeningPort:number = 3000; /** - * Server's Backend command line arguments. + * Server's Context command line arguments. * * @property _BackendArguments * @type Array diff --git a/backend/src/Context.ts b/backend/src/Context.ts index b102a18..e92e8ae 100644 --- a/backend/src/Context.ts +++ b/backend/src/Context.ts @@ -1,9 +1,208 @@ +import BadgeRepository = require('./badge/BadgeRepository'); +import BadgeFactory = require('./badge/BadgeFactory'); + import GoalRepository = require('./goal/GoalRepository'); -import ChallengeRepository = require('./challenge/UserChallengeRepository'); +import GoalFactory = require('./goal/GoalFactory'); + +import UserChallengeRepository = require('./challenge/UserChallengeRepository'); +import UserChallengeFactory = require('./challenge/UserChallengeFactory'); + import UserRepository = require('./user/UserRepository'); +import UserFactory = require('./user/UserFactory'); +import User = require('./user/User'); + +import TeamRepository = require('./user/TeamRepository'); +import TeamFactory = require('./user/TeamFactory'); + +import Clock = require('./Clock'); + + +import StoringHandler = require('./StoringHandler'); + +class Context { + + + private _badgeRepository:BadgeRepository; + private _badgeFactory:BadgeFactory; + + private _goalRepository:GoalRepository; + private _goalFactory:GoalFactory; + + private _userChallengeRepository:UserChallengeRepository; + private _userChallengeFactory:UserChallengeFactory; + + private _userRepository:UserRepository; + private _userFactory:UserFactory; + + private _teamRepository:TeamRepository; + private _teamFactory:TeamFactory; + + private storingHandler:StoringHandler; + + private _pathToFileToLoad:string; + private _pathToStubFile:string; + + constructor(pathToFileToLoad:string, pathToStubFile:string = null) { + this._pathToFileToLoad = pathToFileToLoad; + this._pathToStubFile = pathToStubFile; + + this._userRepository = new UserRepository(); + + this._badgeRepository = new BadgeRepository(); + this._badgeFactory = new BadgeFactory(); + + this._goalRepository = new GoalRepository(this._badgeRepository); + this._goalFactory = new GoalFactory(); + + this._userChallengeRepository = new UserChallengeRepository(); + this._userChallengeFactory = new UserChallengeFactory(); + + this._userRepository = new UserRepository(); + this._userFactory = new UserFactory(); + + this._teamRepository = new TeamRepository(); + this._teamFactory = new TeamFactory(); + + this.storingHandler = new StoringHandler(this); + } + + loadData() { + var data = this.storingHandler.load(this._pathToFileToLoad); + this.fillRepositories(data); + } + + saveData(successFunc:Function, failFunc:Function) { + this.storingHandler.save(this._pathToFileToLoad, successFunc, failFunc); + } + + fillRepositories(data) { + //console.log("___________________________________________________________"); + + this.fillGoalDefinitionRepository(data); + //this._goalRepository.displayShortState(); + + this.fillBadgesRepository(data); + //this._badgeRepository.displayShortState(); + + this.fillUsersRepository(data); + //this._userRepository.displayShortState(); + + this.fillTeamRepository(data); + //this._teamRepository.displayShortState(); + + this.fillChallengesRepository(data); + //this._userChallengeRepository.displayShortState(); + + //console.log("___________________________________________________________"); + + return {success: '+++\tRepositories filled correctly\t+++'}; + } + + fillGoalDefinitionRepository(data) { + var goalDefinitions = data.definitions; + + for (var currentGoalDefinitionIndex in goalDefinitions) { + var currentGoalDefinition = goalDefinitions[currentGoalDefinitionIndex]; + var currentGoal = this._goalFactory.createGoal(currentGoalDefinition); + this._goalRepository.addGoal(currentGoal); + } + } + + fillBadgesRepository(data) { + var badges = data.badges; + + for (var currentBadgeIndex in badges) { + var currentBadgeDescription = badges[currentBadgeIndex]; + this._badgeRepository.addBadge(this._badgeFactory.createBadge(currentBadgeDescription)); + } + } + + fillUsersRepository(data) { + var users = data.users; + + for (var currentUserIndex in users) { + var currentUserDescription = users[currentUserIndex]; + var currentUser = this._userFactory.createUser(currentUserDescription, this._userChallengeFactory); + this._userRepository.addUser(currentUser); + this._userRepository.setCurrentUser(currentUser); + } + } + + fillTeamRepository(data) { + var teams = data.teams; + + for (var currentTeamIndex in teams) { + var currentTeamDescription = teams[currentTeamIndex]; + var currentTeam = this._teamFactory.createTeam(currentTeamDescription, this._userRepository); + this._teamRepository.addTeam(currentTeam); + } + + } + + fillChallengesRepository(data) { + var challenges = data.challenges; + this.fillUserChallengeRepository(challenges); + } + + fillUserChallengeRepository(data) { + var challenges = data.userChallenges; + + for (var currentChallengeIndex = 0; currentChallengeIndex < challenges.length; currentChallengeIndex++) { + var currentChallengeDescription = challenges[currentChallengeIndex]; + + var currentChallenge = this._userChallengeFactory.restoreChallenge(currentChallengeDescription, this._goalRepository, this._userRepository, Clock.getMoment(Clock.getNow())); + + this._userChallengeRepository.addGoalInstance(currentChallenge); + } + } + + public getBadgeRepository():BadgeRepository { + return this._badgeRepository; + } + + public getBadgeFactory():BadgeFactory { + return this._badgeFactory; + } + + public getGoalRepository():GoalRepository { + return this._goalRepository; + } + + public getGoalFactory():GoalFactory { + return this._goalFactory; + } + + public getUserChallengeRepository():UserChallengeRepository { + return this._userChallengeRepository; + } + + public getUserChallengeFactory():UserChallengeFactory { + return this._userChallengeFactory; + } + + public getUserRepository():UserRepository { + return this._userRepository; + } + + public getUserFactory():UserFactory { + return this._userFactory; + } + + public getTeamRepository():TeamRepository { + return this._teamRepository; + } + + public getTeamFactory():TeamFactory { + return this._teamFactory; + } + + public getPathToFileToLoad():string { + return this._pathToFileToLoad; + } -interface Context { - fill(goalDefinitionRepository:GoalRepository, goalInstanceRepository:ChallengeRepository, userRepository:UserRepository); + public getPathToStubFile():string { + return this._pathToStubFile; + } } export = Context; \ No newline at end of file diff --git a/backend/src/JSONSerializer.ts b/backend/src/JSONSerializer.ts index add756c..681e643 100644 --- a/backend/src/JSONSerializer.ts +++ b/backend/src/JSONSerializer.ts @@ -4,25 +4,23 @@ var fs = require('fs'); class JSONSerializer { - public static JSON_DB_FILE:string = 'db.json'; - - public load():any { - if (!fs.existsSync(JSONSerializer.JSON_DB_FILE)) { - return {error: 'File ' + JSONSerializer.JSON_DB_FILE + ' not found'}; + public load(pathToFileToLoad:string):any { + if (!fs.existsSync(pathToFileToLoad)) { + throw new Error('File ' + pathToFileToLoad + ' not found'); } - var data = fs.readFileSync(JSONSerializer.JSON_DB_FILE, "utf-8"); + var data = fs.readFileSync(pathToFileToLoad, "utf-8"); if (Object.keys(JSON.parse(data)).length == 0) { return {error: '+++\tDatabase was empty !\t+++', data: data}; } - return {success: '+++\tDatabase loaded correctly !\t+++', data: data}; + return {success: '+++\tDatabase loaded correctly !\t+++', data: JSON.parse(data)}; } - public save(data:any, successCallBack:Function, failCallBack:Function):void { + public save(data:any, pathToFile:string, successCallBack:Function, failCallBack:Function):void { - fs.writeFile(JSONSerializer.JSON_DB_FILE, JSON.stringify(data, null, 2), function (err) { + fs.writeFile(pathToFile, JSON.stringify(data, null, 2), function (err) { if (err) { failCallBack(err); } diff --git a/backend/src/Server.ts b/backend/src/Server.ts index c286105..ee09a7e 100644 --- a/backend/src/Server.ts +++ b/backend/src/Server.ts @@ -42,7 +42,6 @@ class Server { */ httpServer:any; - userRepository:UserRepository; /** * Constructor. @@ -50,10 +49,9 @@ class Server { * @param {number} listeningPort - Listening port. * @param {Array} arguments - Command line arguments. */ - constructor(listeningPort:number, arguments:Array, userRepository:UserRepository) { + constructor(listeningPort:number, arguments:Array) { this.listeningPort = listeningPort; this._buildServer(); - this.userRepository = userRepository; } /** diff --git a/backend/src/StoringHandler.ts b/backend/src/StoringHandler.ts index c2d9c5f..aa79b87 100644 --- a/backend/src/StoringHandler.ts +++ b/backend/src/StoringHandler.ts @@ -1,19 +1,19 @@ import JSONSerializer = require('./JSONSerializer'); -import Backend = require('./Backend'); +import Context = require('./Context'); import Clock = require('./Clock'); class StoringHandler { private serializer:JSONSerializer; - private backend:Backend; + private context:Context; - constructor(backend:Backend) { + constructor(backend:Context) { this.serializer = new JSONSerializer(); - this.backend = backend; + this.context = backend; } - load():any { - var result:any = this.serializer.load(); + load(pathToFileToLoad:string):any { + var result:any = this.serializer.load(pathToFileToLoad); if (result.error) { return result.error; @@ -21,95 +21,19 @@ class StoringHandler { console.log(result.success); - return this.fillRepositories(JSON.parse(result.data)); + return result.data; } - save(successCallBack:Function, failCallBack:Function) { + save(pathToFile:string, successCallBack:Function, failCallBack:Function) { var result:any = {}; - result['definitions'] = this.backend.goalDefinitionRepository.getDataInJSON(); - result['badges'] = this.backend.badgeRepository.getDataInJSON(); - result['users'] = this.backend.userRepository.getDataInJSON(); + result['definitions'] = this.context.getGoalRepository().getDataInJSON(); + result['badges'] = this.context.getBadgeRepository().getDataInJSON(); + result['users'] = this.context.getUserRepository().getDataInJSON(); result['challenges'] = {}; - result['challenges']['userChallenges'] = this.backend.challengeRepository.getDataInJSON(); + result['challenges']['userChallenges'] = this.context.getUserChallengeRepository().getDataInJSON(); - this.serializer.save(result, successCallBack, failCallBack); - } - - fillRepositories(data) { - console.log("___________________________________________________________"); - - this.fillGoalDefinitionRepository(data); - this.backend.goalDefinitionRepository.displayShortState(); - - this.fillBadgesRepository(data); - this.backend.badgeRepository.displayShortState(); - - this.fillUsersRepository(data); - this.backend.userRepository.displayShortState(); - - this.fillTeamRepository(data); - this.backend.teamRepository.displayShortState(); - - this.fillChallengesRepository(data); - this.backend.challengeRepository.displayShortState(); - - console.log("___________________________________________________________"); - - return {success: '+++\tRepositories filled correctly\t+++'}; - } - - fillGoalDefinitionRepository(data) { - var goalDefinitions = data.definitions; - - for (var currentGoalDefinitionIndex in goalDefinitions) { - var currentGoalDefinition = goalDefinitions[currentGoalDefinitionIndex]; - var currentGoal = this.backend.goalDefinitionFactory.createGoal(currentGoalDefinition); - this.backend.goalDefinitionRepository.addGoal(currentGoal); - } - } - - fillBadgesRepository(data) { - var badges = data.badges; - - for (var currentBadgeIndex in badges) { - var currentBadgeDescription = badges[currentBadgeIndex]; - this.backend.badgeRepository.addBadge(this.backend.badgeFactory.createBadge(currentBadgeDescription)); - } - } - - fillUsersRepository(data) { - var users = data.users; - - for (var currentUserIndex in users) { - var currentUserDescription = users[currentUserIndex]; - var currentUser = this.backend.userFactory.createUser(currentUserDescription, this.backend.challengeFactory); - this.backend.userRepository.addUser(currentUser); - this.backend.userRepository.setCurrentUser(currentUser); - } - } - - fillTeamRepository(data) { - var teams = data.teams; - - for (var currentTeamIndex in teams) { - var currentTeamDescription = teams[currentTeamIndex]; - var currentTeam = this.backend.teamFactory.createTeam(currentTeamDescription, this.backend.userRepository); - this.backend.teamRepository.addTeam(currentTeam); - } - - } - - fillChallengesRepository(data) { - var challenges = data.challenges; - - for (var currentChallengeIndex = 0; currentChallengeIndex < challenges.length; currentChallengeIndex++) { - var currentChallengeDescription = challenges[currentChallengeIndex]; - - var currentChallenge = this.backend.challengeFactory.restoreChallenge(currentChallengeDescription, this.backend.goalDefinitionRepository, this.backend.userRepository, Clock.getMoment(Clock.getNow())); - - this.backend.challengeRepository.addGoalInstance(currentChallenge); - } + this.serializer.save(pathToFile, result, successCallBack, failCallBack); } } diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index a907f00..328c704 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -15,11 +15,17 @@ import Team = require('../user/Team'); import Entity = require('../user/Entity'); import Middleware = require('../Middleware'); +import Context = require('../Context'); + +var merge = require('merge'); + +import BadRequestException = require('../exceptions/BadRequestException'); +import BadArgumentException = require('../exceptions/BadArgumentException'); class DashboardRouter extends RouterItf { public static DEMO:boolean = true; private jsonStub:any = {}; - public static STUB_FILE:string = './stub_values.json'; + private pathToStubFile:string; private challengeRepository:ChallengeRepository; private challengeFactory:ChallengeFactory; @@ -37,20 +43,22 @@ class DashboardRouter extends RouterItf { // TODO DELETE THIS TOKEN WHEN SESSION WILL BE ESTABLISHED private currentUser:Entity; - constructor(challengeRepository:ChallengeRepository, challengeFactory:ChallengeFactory, goalRepository:GoalRepository, - userRepository:UserRepository, teamRepository:TeamRepository, badgeRepository:BadgeRepository, middleware:Middleware) { + constructor(context:Context, middleware:Middleware) { super(); + this.pathToStubFile = context.getPathToStubFile(); + var fs = require('fs'); - var data = fs.readFileSync(DashboardRouter.STUB_FILE, "utf-8"); + var data = fs.readFileSync(this.pathToStubFile, "utf-8"); this.jsonStub = JSON.parse(data); - this.challengeRepository = challengeRepository; - this.challengeFactory = challengeFactory; - this.goalRepository = goalRepository; - this.userRepository = userRepository; - this.teamRepository = teamRepository; - this.badgeRepository = badgeRepository; + this.challengeRepository = context.getUserChallengeRepository(); + this.challengeFactory = context.getUserChallengeFactory(); + this.goalRepository = context.getGoalRepository(); + this.userRepository = context.getUserRepository(); + this.teamRepository = context.getTeamRepository(); + this.badgeRepository = context.getBadgeRepository(); + this.middleware = middleware; } @@ -119,7 +127,7 @@ class DashboardRouter extends RouterItf { self.getDashboard(req, res); }); - this.router.delete('/delete/:id', function (req, res) { + this.router.delete('/delete/:id/:challengeID', function (req, res) { self.deleteChallenge(req, res); }); @@ -173,24 +181,42 @@ class DashboardRouter extends RouterItf { res.status(400).send({'error': 'goalID field is missing in request'}); } - // TODO replace currentUser by user in the session - var currentUser = this.userRepository.getCurrentUser(); + var currentUser = req.params.id; - var newChall:Challenge = this.createGoalInstance(currentUser, goalID, Clock.getMoment(Clock.getNow())); + var newChallenge:Challenge = this.createChallenge(currentUser, goalID, Clock.getMoment(Clock.getNow())); - if (newChall == null) { + if (newChallenge == null) { res.send({'error': 'Can not take this challenge'}); return; } - res.send({"success": ("Objectif ajouté !" + newChall.getDataInJSON())}); + res.send({"success": ("Objectif ajouté !" + newChallenge.getDataInJSON())}); } deleteChallenge(req:any, res:any) { - var goalID = req.params.id; + var challengeID = req.params.challengeID; + var userID = req.params.id; + var user:User = null; try { - this.currentUser.deleteChallenge(goalID); + user = this.checkUserProfile(userID); + } + catch(e) { + if(e instanceof BadRequestException) { + res.status(400).send({error:e.getMessage()}); + + } + else if(e instanceof BadArgumentException) { + res.status(400).send({error:e.getMessage()}); + + } + else { + res.status(500).send({error:e.getMessage()}); + } + } + + try { + user.deleteChallenge(challengeID); res.send({"success": "Objectif supprimé !"}); } catch (e) { @@ -308,7 +334,7 @@ class DashboardRouter extends RouterItf { // Evaluate challenge and return them // Done before everything to be up to date - this.evaluateChallengeForGivenUser(user); + this.evaluateChallengesForGivenUser(user); // First col : available goal var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(user, this.challengeRepository); @@ -327,33 +353,12 @@ class DashboardRouter extends RouterItf { return result; } - - private evaluateChallengeForGivenTeam(team:Team):void { - var challenges = team.getCurrentChallenges(); - for (var challengeIndex in challenges) { - var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); - - this.evaluateChallenge(team, currentChallenge, currentChallengeID); - } - } - - private evaluateChallengeForGivenUser(user:User):void { - var challenges = user.getCurrentChallenges(); - for (var challengeIndex in challenges) { - var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); - - this.evaluateChallenge(user, currentChallenge, currentChallengeID); - } - } - private buildCurrentChallengesDescriptionForGivenEntity(team:Team):any[] { var descriptionOfChallenges:any[] = []; var challenges = team.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); var currentChallengeDesc = currentChallenge.getDataInJSON(); descriptionOfChallenges.push(currentChallengeDesc); } @@ -361,14 +366,13 @@ class DashboardRouter extends RouterItf { return descriptionOfChallenges; } - private buildCurrentChallengesDescriptionForGivenUser(user:User):any[] { var descriptionOfChallenges:any[] = []; var challenges = user.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getGoalInstance(currentChallengeID); + var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); var currentChallengeDesc = currentChallenge.getDataInJSON(); descriptionOfChallenges.push(currentChallengeDesc); } @@ -376,7 +380,6 @@ class DashboardRouter extends RouterItf { return descriptionOfChallenges; } - private buildBadgesDescriptionForGivenEntity(team:Team):any[] { var descriptionOfBadges:any[] = []; @@ -395,7 +398,6 @@ class DashboardRouter extends RouterItf { return descriptionOfBadges; } - private buildBadgesDescriptionForGivenUser(user:User):any[] { var descriptionOfBadges:any[] = []; @@ -414,6 +416,27 @@ class DashboardRouter extends RouterItf { return descriptionOfBadges; } + private evaluateChallengeForGivenTeam(team:Team):void { + var challenges = team.getCurrentChallenges(); + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); + + this.evaluateChallenge(team, currentChallenge, currentChallengeID); + } + } + + evaluateChallengesForGivenUser(user:User):void { + var challenges = user.getCurrentChallenges(); + + for (var challengeIndex in challenges) { + var currentChallengeID = challenges[challengeIndex]; + var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); + + this.evaluateChallenge(user, currentChallenge, currentChallengeID); + } + } + private evaluateChallenge(entity, challengeToEvaluate:Challenge, challengeID) { var self = this; @@ -437,14 +460,14 @@ class DashboardRouter extends RouterItf { function () { var result = challengeToEvaluate.evaluate(required); if (result) { - var newChall = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChall = self.createChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); this.addBadge(challengeID, entity.getUUID()); if (newChall != null) { self.evaluateChallenge(entity, newChall, newChall.getId()); } } console.log("All data were retrieve properly"); - return challengeToEvaluate.getProgress(); + return challengeToEvaluate; }, function () { return {error: "Error occurred in middleware"}; @@ -457,39 +480,48 @@ class DashboardRouter extends RouterItf { console.log('++++++++++++++++++++++ \tMODE DEMO\t+++++++++++++++++++++'); if (challengeToEvaluate.haveToStart(Clock.getCurrentDemoMoment())) { + console.log("Le challenge doit démarrer"); challengeToEvaluate.setStatus(ChallengeStatus.RUN); } - var result = challengeToEvaluate.evaluate(this.jsonStub); + for(var currentConditionID in challengeToEvaluate.getSensors()) { + var currentConditionDesc = challengeToEvaluate.getSensors()[currentConditionID]; + currentConditionDesc = merge(currentConditionDesc, this.jsonStub); + } + + var evaluateData = challengeToEvaluate.getSensors(); + + var result:any = challengeToEvaluate.evaluate(evaluateData); + // Check if the challenge is achieved and finished - if (result && challengeToEvaluate.isFinished()) { + if (challengeToEvaluate.getStatus() == ChallengeStatus.SUCCESS) { console.log("Le challenge est réussi et terminé"); // Add finished badge to current user this.addFinishedBadge(challengeID, entity.getUUID()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } } // Check if the challenge is not achieved but finished - else if (!result && challengeToEvaluate.isFinished()) { + else if (challengeToEvaluate.getStatus() == ChallengeStatus.FAIL) { console.log("Le challenge est FAIL et terminé"); entity.deleteChallenge(challengeToEvaluate.getId()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createGoalInstance(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); } } - return challengeToEvaluate.getProgress(); + return challengeToEvaluate; } } @@ -506,11 +538,13 @@ class DashboardRouter extends RouterItf { user.deleteChallenge(challengeID); } - createGoalInstance(currentUser:User, goalID:string, date:moment.Moment):Challenge { + createChallenge(userID:string, goalID:string, date:moment.Moment):Challenge { + + var user:User = this.userRepository.getUser(userID); var goal:Goal = this.goalRepository.getGoal(goalID); - var newChallenge = currentUser.addChallenge(goal, date); + var newChallenge = user.addChallenge(goal, date); if (newChallenge == null) { return null; @@ -519,6 +553,21 @@ class DashboardRouter extends RouterItf { this.challengeRepository.addGoalInstance(newChallenge); return newChallenge; } + + + checkUserProfile(username):User { + + if (!username) { + throw new BadRequestException('Field username is missing in request'); + } + + var currentUser:User = this.userRepository.getUserByName(username); + if (currentUser == null) { + throw new BadArgumentException('Given username can not be found'); + } + + return currentUser; + } } export = DashboardRouter; \ No newline at end of file diff --git a/backend/src/api/LoginRouter.ts b/backend/src/api/LoginRouter.ts index 7f085a1..3750fb9 100644 --- a/backend/src/api/LoginRouter.ts +++ b/backend/src/api/LoginRouter.ts @@ -2,13 +2,16 @@ import RouterItf = require('./RouterItf'); import UserRepository = require('../user/UserRepository'); import User = require('../user/User'); +import Context = require('../Context'); +import BadRequestException = require('../exceptions/BadRequestException'); +import BadArgumentException = require('../exceptions/BadArgumentException'); class LoginRouter extends RouterItf { private userRepository:UserRepository; - constructor(userRepository:UserRepository) { + constructor(context:Context){ super(); - this.userRepository = userRepository; + this.userRepository = context.getUserRepository(); } buildRouter() { @@ -18,24 +21,44 @@ class LoginRouter extends RouterItf { }); this.router.post('/', function (req, res) { - console.log('Data received ', req.body); + var userProfile:User = null; - console.log('Login :', req.body.username); - - if(!req.body.username) { - res.status(400).send({error:'Field username is missing in request'}); - return; + try { + userProfile = self.checkUserProfile(req.body); + res.send({success:'User profile was found', data:{token:userProfile.getUUID()}}); } + catch(e) { + if(e instanceof BadRequestException) { + res.status(400).send({error:e.getMessage()}); + + } + else if(e instanceof BadArgumentException) { + res.status(400).send({error:e.getMessage()}); - var currentUser:User = self.userRepository.getUserByName(req.body.username); - if(currentUser == null) { - res.status(400).send({error:'Given username can not be found'}); - return; + } + else { + res.status(500).send({error:e.getMessage()}); + } } - res.send({success:'User profile was found', data:{token:currentUser.getUUID()}}); }); } + + checkUserProfile(data):User { + console.log('Data received ', data); + console.log('Login :', data.username); + + if(!data.username) { + throw new BadRequestException('Field username is missing in request'); + } + + var currentUser:User = this.userRepository.getUserByName(data.username); + if(currentUser == null) { + throw new BadArgumentException('Given username can not be found'); + } + + return currentUser; + } } export = LoginRouter; \ No newline at end of file diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 290a673..023911d 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -21,9 +21,7 @@ class UserChallenge { private status:BadgeStatus; - private progress:any[] = []; - private progressDescription:any = {}; - private percentageOfTime:number = 0; + private progress:any = {}; // { A_CONDITION_ID : { symbolic_name: tmp_cli, timeBox: { startDate:..., endDate } } } private mapConditionIDToSensorAndTimeBoxRequired:any = {}; @@ -47,13 +45,16 @@ class UserChallenge { this.mapConditionIDToSensorAndTimeBoxRequired = mapConditionIDToSensorAndTimeBoxRequired; this.mapSymbolicNameToSensor = this.user.getMapSymbolicNameToSensor(); + + this.buildRequired(); + } getConditionDescriptionByID(conditionID:string) { return this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; } - public updateDurationAchieved(currentDate:number) { + public updateDurationAchieved(currentDate:number):number { var current:moment.Moment = Clock.getMoment(currentDate); @@ -63,43 +64,18 @@ class UserChallenge { var duration:number = this.endDate.valueOf() - this.startDate.valueOf(); var durationAchieved:number = current.valueOf() - this.startDate.valueOf(); - this.percentageOfTime = durationAchieved * 100 / duration; + var percentageOfTime = durationAchieved * 100 / duration; // It can have tiny incorrect decimal values - this.percentageOfTime = (this.percentageOfTime > 100) ? 100 : this.percentageOfTime; + percentageOfTime = (percentageOfTime > 100) ? 100 : percentageOfTime; + + return percentageOfTime; } getUser():User { return this.user; } - isFinished():boolean { - return this.getTimeProgress() >= 100; - } - - getTimeProgress():number { - return this.percentageOfTime; - } - - resetProgress() { - this.progress = []; - } - - addProgressByCondition(conditionID:string, percentageAchieved:number) { - this.progressDescription[conditionID] = percentageAchieved; - } - - getGlobalProgression():number { - var globalProgression:number = 0; - - for (var currentConditionID in this.progressDescription) { - var currentConditionProgression = this.progressDescription[currentConditionID]; - globalProgression += currentConditionProgression; - } - - return globalProgression / (Object.keys(this.progressDescription)).length; - } - getStartDate():moment.Moment { return this.startDate; } @@ -128,10 +104,6 @@ class UserChallenge { return this.id === aUUID; } - getProgress():any { - this.progress['global'] = this.getGlobalProgression(); - return this.progress; - } getStatus():BadgeStatus { return this.status; @@ -146,13 +118,18 @@ class UserChallenge { } getSensors():any { + return this.mapConditionIDToSensorAndTimeBoxRequired; + } + + private buildRequired():any { - for(var conditionID in this.mapConditionIDToSensorAndTimeBoxRequired) { + for (var conditionID in this.mapConditionIDToSensorAndTimeBoxRequired) { var sensors:string[] = []; var currentConditionDescription = this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; var symbolicNames:string[] = currentConditionDescription.symbolicNames; - for(var symbolicNamesIndex in symbolicNames) { + + for (var symbolicNamesIndex in symbolicNames) { var currentSymbolicName = symbolicNames[symbolicNamesIndex]; var currentSensor = this.mapSymbolicNameToSensor[currentSymbolicName]; sensors.push(currentSensor); @@ -176,64 +153,66 @@ class UserChallenge { /** * * @param values - * - * - describing a required of a condition + * { + * : * { - * 'name' : - symbolic name of the required field, eg : 'Temp_cli', - * 'sensor' : 'sensor_id ', - sensor id bound to achieve current goal condition, eg : 'AC_443', - * 'value' : - current value of specified sensor + * symbolicNames:[...], + * : [ ] * } - * @returns {boolean} + * } + * @returns {any} */ - evaluate(values:any):boolean { - console.log('evaluate de challenge'); + evaluate(values:any):any { + + for(var currentConditionID in values) { + var currentConditionDesc = values[currentConditionID]; + currentConditionDesc['values'] = {}; + for(var currentSymbolicNameIndex in currentConditionDesc.symbolicNames) { + var currentSymbolicName = currentConditionDesc.symbolicNames[currentSymbolicNameIndex]; + var sensorNameBound = this.mapSymbolicNameToSensor[currentSymbolicName]; + if(sensorNameBound == null) continue; + var dataForCurrentSensor = currentConditionDesc[sensorNameBound]; + currentConditionDesc['values'][currentSymbolicName] = dataForCurrentSensor; + } + } + // Check if badge is running. If Waiting or failed, it must be left unchanged if (this.status != BadgeStatus.RUN) { return false; } - this.resetProgress(); - - this.updateDurationAchieved(Clock.getNow()); - var numberOfValues = Object.keys(values).length; - var numberOfValuesNeeded = Object.keys(this.mapSymbolicNameToSensor).length; - - if (numberOfValues < numberOfValuesNeeded) { - throw new Error("Can not evaluate goal " + this.goal.getName() - + "! There are " + numberOfValuesNeeded + " symbolic names needed and only " - + numberOfValues + " values given"); - } - - var mapSymbolicNameToValue = this.bindSymbolicNameToValue(values); - + var resultEval = this.goal.evaluate(values, this); - // TODO - // Il faut ajouter une indirection - // MapSymbolicNameTOValue ne fait que TMP_CLI => [val1, val2] - // Il faut en fait faire CONDITION_ID => { TMP_CLI => [val1] } - // Seul moyen pour que ça fonctionne ! - // Le merge de timebox s'est fait ; il faut rajouter un - // "isInTimeBox" dans bindSNTV pour reconstuire le schéma - // cID => SN => Vals + var durationAchieved:number = this.updateDurationAchieved(Clock.getNow()); + resultEval['durationAchieved'] = durationAchieved; + var finished:boolean = (durationAchieved === 100) ? true : false; + resultEval['finished'] = finished; + var achieved:boolean = resultEval['achieved']; - - - var resultEval = this.goal.evaluate(mapSymbolicNameToValue, this); - - if (resultEval && this.percentageOfTime >= 100) { + if (achieved && finished) { this.status = BadgeStatus.SUCCESS; console.log('success!'); return true; - } else if (this.percentageOfTime >= 100) { + } else if (!achieved && finished) { this.status = BadgeStatus.FAIL; console.log('Fail!'); } else { console.log('run'); this.status = BadgeStatus.RUN; } - return false; + + this.progress = resultEval; + + return resultEval; + } + getGlobalProgression():any { + return this.progress; + } + + getTimeProgress():any { + return this.progress['durationAchieved']; } bindSymbolicNameToValue(mapSensorToValue:any) { @@ -250,9 +229,7 @@ class UserChallenge { } - getDataInJSON():any { - console.log('time progress : ', this.percentageOfTime); return { id: this.id, startDate: this.startDate, diff --git a/backend/src/challenge/UserChallengeFactory.ts b/backend/src/challenge/UserChallengeFactory.ts index e3752b0..ae8cdb4 100644 --- a/backend/src/challenge/UserChallengeFactory.ts +++ b/backend/src/challenge/UserChallengeFactory.ts @@ -63,7 +63,8 @@ class GoalInstanceFactory { var currentCondition = goalConditions[conditionIndex]; var conditionID = currentCondition.getID(); var symbolicNamesAndTimeBoxRequired = currentCondition.getRequiredByCondition(startDateOfChallenge, endDateOfChallenge); - mapConditionIDToSensorAndTimeBoxRequired[conditionID] = symbolicNamesAndTimeBoxRequired; + + mapConditionIDToSensorAndTimeBoxRequired[conditionID] = symbolicNamesAndTimeBoxRequired; } diff --git a/backend/src/challenge/UserChallengeRepository.ts b/backend/src/challenge/UserChallengeRepository.ts index 14e17c8..8e35dcd 100644 --- a/backend/src/challenge/UserChallengeRepository.ts +++ b/backend/src/challenge/UserChallengeRepository.ts @@ -25,14 +25,14 @@ class BadgeProvider { } public getBadgeByChallengeID(challengeID:string):string { - return this.getGoalInstance(challengeID).getBadge(); + return this.getChallengeByID(challengeID).getBadge(); } public addGoalInstance(aChallenge:Challenge) { this.goalInstancesArray.push(aChallenge); } - public getGoalInstance(challengeID:string):Challenge { + public getChallengeByID(challengeID:string):Challenge { for (var i in this.goalInstancesArray) { var currentBadge = this.goalInstancesArray[i]; if (currentBadge.hasUUID(challengeID)) { @@ -66,7 +66,7 @@ class BadgeProvider { currentBadgeDesc.name = this.goalInstancesArray[i].getName(); currentBadgeDesc.id = this.goalInstancesArray[i].getId(); currentBadgeDesc.desc = this.goalInstancesArray[i].getGoal().getName(); - currentBadgeDesc.progress = this.goalInstancesArray[i].getProgress(); + currentBadgeDesc.progress = this.goalInstancesArray[i].getGlobalProgression(); currentBadgeDesc.startDate = this.goalInstancesArray[i].getStartDate(); currentBadgeDesc.endDate = this.goalInstancesArray[i].getEndDate(); diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index 234aefe..b186bc1 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -84,7 +84,7 @@ class AverageOnValue extends Condition { } var finished:boolean = percentageAchieved === 100; - var result:any = {percentageAchieved: percentageAchieved, finished: finished}; + var result:any = {description: this.description, percentageAchieved: percentageAchieved, finished: finished}; return result; diff --git a/backend/src/condition/OverallGoalCondition.ts b/backend/src/condition/OverallGoalCondition.ts index c28f757..d1410ec 100644 --- a/backend/src/condition/OverallGoalCondition.ts +++ b/backend/src/condition/OverallGoalCondition.ts @@ -18,7 +18,7 @@ class OverallGoalCondition extends Condition { super(id, description, condition, thresholdRate, filter); } - public evaluate(data:any, conditionDescription:any) { + public evaluate(data:any, conditionDescription:any):any { var remainingData:any = super.keepUsefulValues(data, conditionDescription); @@ -51,19 +51,19 @@ class OverallGoalCondition extends Condition { // Check value by value if internal condition is satisfied if (this.expression.evaluate(dataToEvaluate)) { + console.log("OK"); ++numberOfCorrectValues; } } } - var percentageAchieved = ((numberOfCorrectValues * 100 / numberOfValues) * 100) / this.thresholdRate; percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; - var finished:boolean = percentageAchieved === 100; + var achieved:boolean = percentageAchieved === 100; - return {percentageAchieved: percentageAchieved, finished: finished}; + return {description: this.description, percentageAchieved: percentageAchieved, achieved: achieved}; } public getDataInJSON():any { diff --git a/backend/src/condition/factory/ConditionFactory.ts b/backend/src/condition/factory/ConditionFactory.ts index fe2f123..8e2d84e 100644 --- a/backend/src/condition/factory/ConditionFactory.ts +++ b/backend/src/condition/factory/ConditionFactory.ts @@ -4,6 +4,7 @@ var moment = require('moment'); var moment_timezone = require('moment-timezone'); +import UUID = require('node-uuid'); import Condition = require('../Condition'); import GoalExpression = require('../expression/GoalExpression'); @@ -35,7 +36,8 @@ class ConditionFactory { } public createOverall(data:any):Condition { - + var id = (data.id == null) ? UUID.v4() : data.id; + var description:string = data.description; var goalExpression:GoalExpression = this.expressionFactory.createExpression(data.expression); @@ -44,12 +46,16 @@ class ConditionFactory { var dayOfWeekFilterDesc:string = data.filter.dayOfWeekFilter; var periodOfDayFilterDesc:string[] = data.filter.periodOfDayFilter; + var filter:Filter = new Filter(dayOfWeekFilterDesc, periodOfDayFilterDesc); - var overallCondition:OverallGoalCondition = new OverallGoalCondition(null, '', goalExpression, threshold, filter); + var overallCondition:OverallGoalCondition = new OverallGoalCondition(id, description, goalExpression, threshold, filter); return overallCondition; } public createComparison(data:any):Condition { + var id = (data.id == null) ? UUID.v4() : data.id; + var description:string = data.description; + var dayOfWeekFilterDesc:string = data.filter.dayOfWeekFilter; var periodOfDayFilterDesc:string[] = data.filter.periodOfDayFilter; var filter:Filter = new Filter(dayOfWeekFilterDesc, periodOfDayFilterDesc); @@ -58,7 +64,7 @@ class ConditionFactory { var referencePeriod:ReferencePeriod = new ReferencePeriod(referencePeriodDesc.numberOfUnitToSubtract, referencePeriodDesc.unitToSubtract); var goalExpression:GoalExpression = this.expressionFactory.createExpression(data.expression); - var averageOnValue:AverageOnValue = new AverageOnValue(null, '', goalExpression, data.threshold, filter, referencePeriod); + var averageOnValue:AverageOnValue = new AverageOnValue(id, description, goalExpression, data.threshold, filter, referencePeriod); return averageOnValue; } diff --git a/backend/src/context/DemoContext.ts b/backend/src/context/DemoContext.ts index 4d5a002..c6c3c7e 100644 --- a/backend/src/context/DemoContext.ts +++ b/backend/src/context/DemoContext.ts @@ -13,62 +13,8 @@ import Operand = require('../condition/expression/Operand'); import Challenge = require('../challenge/UserChallenge'); -class DemoContext implements Context { +class DemoContext { - private aGoal:Goal; - private aUUID:string; - - - fill(goalDefinitionRepository:GoalRepository, goalInstanceRepository:ChallengeRepository, userRepository:UserRepository) { - if (goalDefinitionRepository) { - this.fillGoalProvider(goalDefinitionRepository); - } - - if(goalInstanceRepository) { - this.fillBadgeProvider(goalInstanceRepository); - } - } - - public fillGoalProvider(goalProvider:GoalRepository) { - /*FIXME - this.aUUID = UUID.v4(); - - this.aGoal = new GoalDefinition('Clim éco !'); - this.aGoal.setUUID(this.aUUID); - - this.aGoal.addCondition(new GoalCondition(new Operand('Temp_cli', true), '>', new Operand('15', false), - 'la température de la clim doit être supérieure à 14°C')); - this.aGoal.addCondition(new GoalCondition(new Operand('Temp_ext', true), '>', new Operand('40', false), - 'la température extérieure doit être supérieure à 40°C')); - - goalProvider.addChallenge(this.aGoal); - */ - } - - public fillBadgeProvider(badgeProvider:ChallengeRepository) { - /* - var mapGoalToConditionAndSensor:any = {}; - - - var condition1Desc:any = {}; - condition1Desc.name = 'Temp_cli'; - condition1Desc.sensor = 'AC_443'; - - var condition2Desc:any = {}; - condition2Desc.name = 'Temp_ext'; - condition2Desc.sensor = 'TEMP_444'; - - var arrayOfConditions:any[] = [condition1Desc, condition2Desc]; - - - mapGoalToConditionAndSensor[this.aUUID.toString()] = arrayOfConditions; - var aBadge = new GoalInstance("Vous n'êtes pas un esquimau !", - this.aGoal,mapGoalToConditionAndSensor); - - badgeProvider.addGoalInstance(aBadge); - FIXME - */ - } } export = DemoContext; \ No newline at end of file diff --git a/backend/src/context/TestContext.ts b/backend/src/context/TestContext.ts new file mode 100644 index 0000000..d4fd35f --- /dev/null +++ b/backend/src/context/TestContext.ts @@ -0,0 +1,3 @@ +/** + * Created by Benjamin on 01/09/2015. + */ diff --git a/backend/src/exceptions/BadArgumentException.ts b/backend/src/exceptions/BadArgumentException.ts index 89caa54..554a52a 100644 --- a/backend/src/exceptions/BadArgumentException.ts +++ b/backend/src/exceptions/BadArgumentException.ts @@ -6,6 +6,10 @@ class BadArgumentException implements Error { this.name = "BadArgumentException"; this.message = message; } + + getMessage():string { + return this.message; + } } export = BadArgumentException; \ No newline at end of file diff --git a/backend/src/exceptions/BadRequestException.ts b/backend/src/exceptions/BadRequestException.ts index 1966aef..9806943 100644 --- a/backend/src/exceptions/BadRequestException.ts +++ b/backend/src/exceptions/BadRequestException.ts @@ -6,6 +6,10 @@ class BadRequestException implements Error { this.name = "BadRequestException"; this.message = message; } + + getMessage():string { + return this.message; + } } export = BadRequestException; \ No newline at end of file diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index 96c15ba..edbbdfc 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -83,22 +83,32 @@ class Goal { this.conditionsArray.push(condition); } - public evaluate(values:any, challenge:Challenge):boolean { + public evaluate(data:any, challenge:Challenge):any { - challenge.resetProgress(); + var result:any = {}; + + var numberOfConditions:number = 0; + var numberOfConditionsAchieved:number = 0; + + var achieved:boolean = true; - var result:boolean = true; for (var i = 0; i < this.conditionsArray.length; i++) { var currentCondition:Condition = this.conditionsArray[i]; - var currentConditionDescription:any = challenge.getConditionDescriptionByID(currentCondition.getID()); - var currentConditionState = currentCondition.evaluate(values,currentConditionDescription); + var currentConditionDescription:any = data[currentCondition.getID()]; + var currentConditionState = currentCondition.evaluate(currentConditionDescription.values, currentConditionDescription); - result = result && currentConditionState.finished; + achieved = achieved && currentConditionState.achieved; + result[currentCondition.getID()] = currentConditionState; - challenge.addProgressByCondition(currentCondition.getID(), currentConditionState.percentageAchieved); + numberOfConditions++; + numberOfConditionsAchieved = (currentConditionState.achieved)?numberOfConditionsAchieved+1 : numberOfConditionsAchieved; } + var percentageAchieved:number = (numberOfConditionsAchieved * 100) / numberOfConditions; + result['percentageAchieved'] = percentageAchieved; + result['achieved'] = achieved; + return result; } diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index df4a36a..73247f5 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -69,7 +69,7 @@ class GoalDefinitionRepository { var takenGoals:Goal[] = []; for (var currentChallengeIDIndex in currentChallengesID) { var currentChallengeID:string = currentChallengesID[currentChallengeIDIndex]; - var currentChallenge:Challenge = challengeRepository.getGoalInstance(currentChallengeID); + var currentChallenge:Challenge = challengeRepository.getChallengeByID(currentChallengeID); takenGoals.push(currentChallenge.getGoal()); } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index 8fc8332..d8c51ed 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -79,6 +79,10 @@ class User { return this.currentChallenges; } + wipeCurrentChallenges():void { + this.currentChallenges = []; + } + addChallenge(goal:Goal, now:moment.Moment):Challenge { var newChallenge = this.challengeFactory.createChallenge(goal, this, now); diff --git a/backend/stub_values.json b/backend/stub_values.json index 6713694..a61b6d4 100644 --- a/backend/stub_values.json +++ b/backend/stub_values.json @@ -1,14 +1,12 @@ { - "TEMP_443V": { - "values": [ - { - "date": "1440072603000", - "value": 20 - }, + "TEMP_443V": [ { - "date":"1440072603000", + "date": "2015-08-03T12:00:00.000Z", + "value": 20 + }, + { + "date":"2015-08-04T12:00:00.000Z", "value":22 } - ] - } + ] } \ No newline at end of file diff --git a/backend/stub_values_test.json b/backend/stub_values_test.json new file mode 100644 index 0000000..a61b6d4 --- /dev/null +++ b/backend/stub_values_test.json @@ -0,0 +1,12 @@ +{ + "TEMP_443V": [ + { + "date": "2015-08-03T12:00:00.000Z", + "value": 20 + }, + { + "date":"2015-08-04T12:00:00.000Z", + "value":22 + } + ] +} \ No newline at end of file diff --git a/backend/tests/UserTest.ts b/backend/tests/UserTest.ts index 6f412f2..4eff80b 100644 --- a/backend/tests/UserTest.ts +++ b/backend/tests/UserTest.ts @@ -24,6 +24,7 @@ describe('User test', () => { }); }); + //TODO TESTS }); diff --git a/backend/tests/condition/OverallGoalConditionTest.ts b/backend/tests/condition/OverallGoalConditionTest.ts index 9f650af..3b0fdfc 100644 --- a/backend/tests/condition/OverallGoalConditionTest.ts +++ b/backend/tests/condition/OverallGoalConditionTest.ts @@ -71,7 +71,7 @@ describe('Test OverallGoalCondition', () => { data[aSymbolicName] = values; var result = condition.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.false; + chai.expect(result.achieved).to.be.false; }); it('should return false if min threshold is not reached', () => { @@ -92,7 +92,7 @@ describe('Test OverallGoalCondition', () => { data[aSymbolicName] = values; var result = condition.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.false; + chai.expect(result.achieved).to.be.false; }); it('should return true if min threshold is just reached', () => { @@ -114,7 +114,7 @@ describe('Test OverallGoalCondition', () => { data[aSymbolicName] = values; var result = condition.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); @@ -137,6 +137,6 @@ describe('Test OverallGoalCondition', () => { data[aSymbolicName] = values; var result = condition.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); }); \ No newline at end of file diff --git a/backend/tests/db_test.json b/backend/tests/db_test.json new file mode 100644 index 0000000..0e640cf --- /dev/null +++ b/backend/tests/db_test.json @@ -0,0 +1,139 @@ +{ + "definitions": [ + { + "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "name": "Clim", + "validityPeriod": { + "start": "2015-08-03T22:00:00.000Z", + "end": "2015-09-03T22:00:00.000Z" + }, + "recurringPeriod": "week", + "conditions": [ + { + "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": "15", + "symbolicName": false + }, + "comparison": ">", + "description": "a desc", + "periodOfTime": "-2208474000000" + }, + "threshold": 25, + "referencePeriod": { + "numberOfUnitToSubtract": 1, + "unitToSubtract": "week" + }, + "filter": { + "dayOfWeekFilter": "all", + "periodOfDayFilter": [ + "morning", + "afternoon" + ] + }, + "type": "comparison" + } + ], + "badgeID": "44bb8108-8830-4f43-abd1-3ef643303d92" + }, + { + "id": "9bddaf87-5065-4df7-920a-d1d249c9171d", + "name": "Obj1", + "validityPeriod": { + "start": "2015-08-04T12:25:57.787Z", + "end": "2015-08-31T12:25:57.787Z" + }, + "recurringPeriod": "week", + "conditions": [ + { + "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": 1, + "symbolicName": false + }, + "comparison": ">", + "description": "tmp_cli > 1" + }, + "threshold": 100, + "filter": { + "dayOfWeekFilter": "working-week", + "periodOfDayFilter": [ + "all" + ] + }, + "type": "overall" + } + ], + "badgeID": "fde68334-f515-4563-954b-ac91b4a42f88" + } + ], + "badges": [ + { + "id": "44bb8108-8830-4f43-abd1-3ef643303d92", + "name": "Un challenge de d\u00e9mo !", + "points": 100 + } + ], + "users": [ + { + "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "name": "Charlie", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + }, + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_443V" + } + }, + { + "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "name": "Gégé", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + }, + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_352" + } + } + ], + "teams": [ + { + "id": "28aa8108-8830-4f43-abd1-3ab643303d92", + "name": "croquette", + "members": [ + "2cf91e02-a320-4766-aa9f-6efce3142d44" + ], + "leader": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + + } + } + ], + "challenges": { + "userChallenges": [ + + ], + "teamChallenges": [ + + ] + } +} \ No newline at end of file diff --git a/backend/tests/goal/GoalTest.ts b/backend/tests/goal/GoalTest.ts index 5cdec54..ecdc73c 100644 --- a/backend/tests/goal/GoalTest.ts +++ b/backend/tests/goal/GoalTest.ts @@ -5,14 +5,15 @@ /// /// -var moment = require('moment'); -var moment_timezone = require('moment-timezone'); import chai = require('chai'); import sinon = require('sinon'); var assert = chai.assert; +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + import Goal = require('../../src/goal/Goal'); import GoalExpression = require('../../src/condition/expression/GoalExpression'); import OverallGoalCondition = require('../../src/condition/OverallGoalCondition'); @@ -26,9 +27,9 @@ describe('Goal Test', () => { var aGoalName:string = "goal 1"; var aBadgeID:string = 'badge 1'; - var now:moment.Moment = moment('2015-08-26T00:00:00'); - var startDate:moment.Moment = moment("2015-08-17T:00:00:00"); - var endDate:moment.Moment = moment("2015-09-17T:23:59:59"); + var now:moment.Moment = moment('2015-08-26T00:00:00.000'); + var startDate:moment.Moment = moment("2015-08-17T00:00:00.000"); + var endDate:moment.Moment = moment("2015-09-17T23:59:59.000"); var aRecurringSession:RecurringSession = new RecurringSession('week'); diff --git a/backend/tests/integration/ChallengeBuildingTest.ts b/backend/tests/integration/ChallengeBuildingTest.ts deleted file mode 100644 index 96b73f8..0000000 --- a/backend/tests/integration/ChallengeBuildingTest.ts +++ /dev/null @@ -1,92 +0,0 @@ -/// -/// -/// -/// - -var moment = require('moment'); - -import chai = require('chai'); -import sinon = require('sinon'); -var assert = chai.assert; - -import BadgeRepository = require('../../src/badge/BadgeRepository'); -import Badge = require('../../src/badge/Badge'); - -import ChallengeRepository = require('../../src/challenge/UserChallengeRepository'); -import ChallengeFactory = require('../../src/challenge/UserChallengeFactory'); -import Challenge = require('../../src/challenge/UserChallenge'); - -import GoalRepository = require('../../src/goal/GoalRepository'); -import Goal = require('../../src/goal/Goal'); -import RecurringSession = require('../../src/goal/RecurringSession'); - -import UserRepository = require('../../src/user/UserRepository'); -import TeamRepository = require('../../src/user/TeamRepository'); - -import User = require('../../src/user/User'); - -import Clock = require('../../src/Clock'); -import ChallengeStatus = require('../../src/Status'); - -import Middleware = require('../../src/Middleware'); - -import DashboardRouter = require('../../src/api/DashboardRouter'); - -describe('UserChallenge integration test', () => { - - /* - FIXME - // Important ! Allow us to set time - DashboardRouter.DEMO = true; - - var badgeRepository:BadgeRepository = new BadgeRepository(); - var challengeRepository:ChallengeRepository = new ChallengeRepository(); - var challengeFactory:ChallengeFactory = new ChallengeFactory(); - var goalRepository:GoalRepository = new GoalRepository(badgeRepository); - - var userRepository:UserRepository = new UserRepository(); - var teamRepository:TeamRepository = new TeamRepository(); - - // Build a default user / current user - var user:User = new User('Charlie'); - userRepository.addUser(user); - userRepository.setCurrentUser(user); - - // Init the router under test - var dashboardRouter:DashboardRouter = new DashboardRouter(challengeRepository, challengeFactory, goalRepository, userRepository, teamRepository, badgeRepository, new Middleware()); - - // Create a fake badge for fake goal - var aBadgeName = 'Badge 1'; - var aBadgePoint = 100; - var aBadge:Badge = new Badge(aBadgeName, aBadgePoint); - - // Create fake goal - var aGoalName = 'Objectif 1'; - var startDate:moment.Moment = moment("August 03, 2015 00:00:00"); - var endDate:moment.Moment = moment("September 03, 2015 00:00:00"); - - var aGoal = new Goal(aGoalName, startDate, endDate, 5, aBadge.getUuid(), null, new RecurringSession('week')); - goalRepository.addGoal(aGoal); - - it('should have initialized the new challenge status to "RUN" when challenge is created during a working week', () => { - var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); - chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.RUN); - }); - - it('should have initialized the new challenge status to "WAITING" when challenge is created during week-end', () => { - // The goal is recurrent every week (monday-friday). A goal created saturday must be in WAITING status - var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-08T12:15:00+02:00")); - chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.WAIT); - }); - - it('should have set the startDate to monday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-05T12:15:00+02:00")); - chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(startDate.toISOString()); - }); - - it('should have set the endDate to friday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createGoalInstance(user, aGoal.getUUID(), moment("2015-08-07T23:59:59+02:00")); - chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(startDate.toISOString()); - }); - */ -}); \ No newline at end of file diff --git a/backend/tests/integration/ChallengeIntegrationTest.ts b/backend/tests/integration/ChallengeIntegrationTest.ts new file mode 100644 index 0000000..a0e5b75 --- /dev/null +++ b/backend/tests/integration/ChallengeIntegrationTest.ts @@ -0,0 +1,114 @@ +/// +/// +/// +/// + +var moment = require('moment'); + +import chai = require('chai'); +import sinon = require('sinon'); +var assert = chai.assert; + +import BadgeRepository = require('../../src/badge/BadgeRepository'); +import Badge = require('../../src/badge/Badge'); + +import UserChallengeRepository = require('../../src/challenge/UserChallengeRepository'); +import UserChallengeFactory = require('../../src/challenge/UserChallengeFactory'); +import UserChallenge = require('../../src/challenge/UserChallenge'); + +import GoalRepository = require('../../src/goal/GoalRepository'); +import Goal = require('../../src/goal/Goal'); +import RecurringSession = require('../../src/goal/RecurringSession'); + +import UserRepository = require('../../src/user/UserRepository'); +import TeamRepository = require('../../src/user/TeamRepository'); + +import User = require('../../src/user/User'); + +import Clock = require('../../src/Clock'); +import ChallengeStatus = require('../../src/Status'); + +import Middleware = require('../../src/Middleware'); + +import DashboardRouter = require('../../src/api/DashboardRouter'); +import LoginRouter = require('../../src/api/LoginRouter'); +import Context = require('../../src/Context'); + +describe('UserChallenge integration test', () => { + + // Important ! Allow us to set time + DashboardRouter.DEMO = true; + + var context:Context = new Context('./db_test.json', './stub_values_test.json'); + + + var dashboardRouter:DashboardRouter = new DashboardRouter(context, null); + var loginRouter:LoginRouter = new LoginRouter(context); + + var aUsername:string = 'Charlie'; + var aGoalID:string = '9bddaf87-5065-4df7-920a-d1d249c9171d'; + + + var requestForLogin:any = { + username: aUsername + }; + + describe('Connection', () => { + context.loadData(); + + it('should have proper id', () => { + var token:string = loginRouter.checkUserProfile(requestForLogin).getUUID(); + var expected:string = '2cf91e02-a320-4766-aa9f-6efce3142d44'; + chai.expect(token).to.be.eq(expected); + }); + }); + + describe('Take a challenge', () => { + + + var user = loginRouter.checkUserProfile(requestForLogin); + var token:string = user.getUUID(); + + beforeEach(() => { + user.wipeCurrentChallenges(); + }); + + it('should not throw', () => { + dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); + }); + + it('should have added given challenge to current user', () => { + var challenge = dashboardRouter.createChallenge(token, aGoalID,Clock.getMomentFromString("2015-08-05T12:15:00")); + var expected = [challenge.getId()]; + var result = user.getCurrentChallenges(); + chai.expect(result).to.be.eqls(expected); + }); + + it('should have initialized the new challenge status to "RUN" when challenge is created during a working week', () => { + var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); + chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.RUN); + }); + + it('should have initialized the new challenge status to "WAITING" when challenge is created during week-end', () => { + var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-08T12:15:00")); + chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.WAIT); + }); + + it('should have set the startDate to monday if goal is "week recurrent"', () => { + var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); + chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(Clock.getMomentFromString("2015-08-03T00:00:00.000").toISOString()); + }); + + it('should have set the endDate to friday if goal is "week recurrent"', () => { + var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); + chai.expect(newChallenge.getEndDate().toISOString()).to.be.eq(Clock.getMomentFromString("2015-08-07T23:59:59.999").toISOString()); + }); + }); + + describe('Evaluate a challenge', () => { + var user = loginRouter.checkUserProfile(requestForLogin); + var challenge = dashboardRouter.createChallenge(user.getUUID(), aGoalID, Clock.getMomentFromString("2015-08-03T12:15:00")); + var result:any = dashboardRouter.evaluateChallengesForGivenUser(user); + + }); +}); \ No newline at end of file From c1f2698ead9926b0f654793001019f7817bc4f6b Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 08:49:00 +0200 Subject: [PATCH 17/28] Team challenge works Features : - A user can log in - A list of all dashboard views available for current user is built - User can switch between dashboard views - The leader of a team can take a challenge for all the team - Switch has been added in dashboard team, allowing leader to take challenges but not members - Members of a team have a new personal challenge when a team challenge is taken - Evaluation of a team challenge works Bug fix : - Routers become state-less, there is no more 'currentUser' field - User can take a personal challenge - Evaluation of a personal challenge works again - User can delete a current challenge - front end dependencies couldn't be satisfied because of different versions of angular, required by different modules -> angular version fixed at ^1.3.0. I had to deletedatepicker dependency, a obsolete module, based of v1..2.14 of angular. It has to be replaced by angular-ui, written by angular devs Todo - you can not delete team challenge - team challenge display incorrect values for percentage achieved by its members. If member#percentageAchieved > 1 && < 100, it will display '0' and if == 100, display '100' - you still can not create a goal from the ui - you still can not create a badge from the ui - team challenges are not saved nor retrieved in the db - team and user classes must inherit from an Entity super class - userChallenge and teamChallenge must inherit from a Challenge super class - Dashboard router must be split into DashboardRouter and Renderer - BadgeRouter and GoalRouter must be re-added - JSONStub must be seen as a 'Source', the same as the Middleware class. DashboardRouter should call this.source.get(sensorname, fromDate, toDate). Warning : This method must be *synchronous* --- backend/Gruntfile.js | 6 +- backend/db.json | 22 +- backend/db_test.json | 4 +- backend/src/Clock.ts | 15 +- backend/src/Context.ts | 27 +- backend/src/Server.ts | 1 - backend/src/api/DashboardRouter.ts | 405 +++++++++++------- backend/src/challenge/TeamChallenge.ts | 169 +++++++- backend/src/challenge/TeamChallengeFactory.ts | 7 +- .../src/challenge/TeamChallengeRepository.ts | 61 ++- backend/src/challenge/UserChallenge.ts | 112 ++++- .../src/challenge/UserChallengeRepository.ts | 152 ++----- backend/src/condition/AverageOnValue.ts | 7 +- backend/src/condition/Condition.ts | 6 +- backend/src/context/DemoContext.ts | 20 - backend/src/context/TestContext.ts | 3 - backend/src/goal/Goal.ts | 6 + backend/src/goal/GoalRepository.ts | 9 +- backend/src/user/Team.ts | 15 +- backend/src/user/TeamFactory.ts | 7 +- backend/src/user/User.ts | 2 +- backend/stub_values.json | 14 +- backend/stub_values_test.json | 4 +- backend/tests/condition/AverageOnValueTest.ts | 397 ++++++++--------- backend/tests/condition/ConditionTest.ts | 36 +- .../condition/OverallGoalConditionTest.ts | 74 ++-- .../integration/ChallengeIntegrationTest.ts | 22 +- frontend/app/index.html | 3 - frontend/app/scripts/app.js | 4 +- .../scripts/controllers/ServiceDashboard.js | 16 +- frontend/app/scripts/controllers/dashboard.js | 27 +- frontend/app/scripts/controllers/login.js | 5 + frontend/app/styles/homepage-challenges.css | 42 +- frontend/app/views/dashboard.html | 6 - .../views/homepage/homepage-challenge.html | 62 +-- .../app/views/homepage/homepage-goal.html | 2 +- frontend/bower.json | 4 +- frontend/test/karma.conf.js | 2 - 38 files changed, 1063 insertions(+), 713 deletions(-) delete mode 100644 backend/src/context/DemoContext.ts delete mode 100644 backend/src/context/TestContext.ts diff --git a/backend/Gruntfile.js b/backend/Gruntfile.js index 39bbb47..c375bdb 100644 --- a/backend/Gruntfile.js +++ b/backend/Gruntfile.js @@ -57,12 +57,12 @@ module.exports = function (grunt) { }, build: { options: { - script: 'build/Context.js' + script: 'build/Backend.js' } }, dist: { options: { - script: 'dist/Context.js', + script: 'dist/Backend.js', node_env: 'production' } } @@ -74,7 +74,7 @@ module.exports = function (grunt) { // --------------------------------------------- watch: { express: { - files: [ 'build/Context.js' ], + files: [ 'build/Backend.js' ], tasks: [ 'express:build' ], options: { spawn: false diff --git a/backend/db.json b/backend/db.json index b45b2d5..87572ef 100644 --- a/backend/db.json +++ b/backend/db.json @@ -107,14 +107,32 @@ "mapSymbolicNameToSensor": { "TMP_CLI":"TEMP_443V" } + }, + { + "id": "6efce3142d44-a320-4766-4766-2cf91e02", + "name": "Gégé", + "currentChallenges": [ + ], + "finishedBadgesMap": { + "44bb8108-8830-4f43-abd1-3ef643303d92": 1, + "fde68334-f515-4563-954b-ac91b4a42f88": 1 + }, + "mapSymbolicNameToSensor": { + "TMP_CLI":"AC_555V" + } } ], "teams": [ { "id" : "28aa8108-8830-4f43-abd1-3ab643303d92", "name" : "croquette", - "members" : ["2cf91e02-a320-4766-aa9f-6efce3142d44"], - "leader":"2cf91e02-a320-4766-aa9f-6efce3142d44" + "members" : ["2cf91e02-a320-4766-aa9f-6efce3142d44", "6efce3142d44-a320-4766-4766-2cf91e02"], + "leader":"2cf91e02-a320-4766-aa9f-6efce3142d44", + "currentChallenges": [ + ], + "finishedBadgesMap": { + "44bb8108-8830-4f43-abd1-3ef643303d92": 2 + } } ], "challenges": diff --git a/backend/db_test.json b/backend/db_test.json index 0e640cf..f2f5bf9 100644 --- a/backend/db_test.json +++ b/backend/db_test.json @@ -11,7 +11,8 @@ "conditions": [ { "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", - "expression": { + "description":"a desc", + "expression": { "valueLeft": { "value": "TMP_CLI", "symbolicName": true @@ -21,7 +22,6 @@ "symbolicName": false }, "comparison": ">", - "description": "a desc", "periodOfTime": "-2208474000000" }, "threshold": 25, diff --git a/backend/src/Clock.ts b/backend/src/Clock.ts index 492676a..45d453b 100644 --- a/backend/src/Clock.ts +++ b/backend/src/Clock.ts @@ -7,16 +7,21 @@ var moment_timezone = require('moment-timezone'); class Clock { - private static now:number = Date.now(); + private static now:moment.Moment = moment(); static getNow():number { - return Clock.now; + return Clock.now.valueOf(); } static setNow(newNow:number) { - Clock.now = newNow; + Clock.now = moment.tz(newNow, Clock.getTimeZone()); } + static setNowByString(newNow:string) { + Clock.now = moment.tz(newNow, Clock.getTimeZone()); + } + + static getTimeZone():string { return 'Europe/Paris'; } @@ -25,6 +30,10 @@ class Clock { return moment.tz(date, Clock.getTimeZone()); } + static getMomentFromUnixTimeInMillis(date:number):moment.Moment { + return moment.tz(date, Clock.getTimeZone()); + } + static getMomentFromString(date:string):moment.Moment { return moment.tz(date, Clock.getTimeZone()); } diff --git a/backend/src/Context.ts b/backend/src/Context.ts index e92e8ae..73475e2 100644 --- a/backend/src/Context.ts +++ b/backend/src/Context.ts @@ -7,6 +7,9 @@ import GoalFactory = require('./goal/GoalFactory'); import UserChallengeRepository = require('./challenge/UserChallengeRepository'); import UserChallengeFactory = require('./challenge/UserChallengeFactory'); +import TeamChallengeRepository = require('./challenge/TeamChallengeRepository'); +import TeamChallengeFactory = require('./challenge/TeamChallengeFactory'); + import UserRepository = require('./user/UserRepository'); import UserFactory = require('./user/UserFactory'); import User = require('./user/User'); @@ -31,6 +34,9 @@ class Context { private _userChallengeRepository:UserChallengeRepository; private _userChallengeFactory:UserChallengeFactory; + private _teamChallengeRepository:TeamChallengeRepository; + private _teamChallengeFactory:TeamChallengeFactory; + private _userRepository:UserRepository; private _userFactory:UserFactory; @@ -57,6 +63,9 @@ class Context { this._userChallengeRepository = new UserChallengeRepository(); this._userChallengeFactory = new UserChallengeFactory(); + this._teamChallengeRepository = new TeamChallengeRepository(); + this._teamChallengeFactory = new TeamChallengeFactory(); + this._userRepository = new UserRepository(); this._userFactory = new UserFactory(); @@ -133,7 +142,7 @@ class Context { for (var currentTeamIndex in teams) { var currentTeamDescription = teams[currentTeamIndex]; - var currentTeam = this._teamFactory.createTeam(currentTeamDescription, this._userRepository); + var currentTeam = this._teamFactory.createTeam(currentTeamDescription, this._userRepository, this._teamChallengeFactory); this._teamRepository.addTeam(currentTeam); } @@ -142,6 +151,7 @@ class Context { fillChallengesRepository(data) { var challenges = data.challenges; this.fillUserChallengeRepository(challenges); + this.fillTeamChallengeRepository(data); } fillUserChallengeRepository(data) { @@ -152,10 +162,15 @@ class Context { var currentChallenge = this._userChallengeFactory.restoreChallenge(currentChallengeDescription, this._goalRepository, this._userRepository, Clock.getMoment(Clock.getNow())); - this._userChallengeRepository.addGoalInstance(currentChallenge); + this._userChallengeRepository.addUserChallenge(currentChallenge); } } + // TODO + fillTeamChallengeRepository(data) { + + } + public getBadgeRepository():BadgeRepository { return this._badgeRepository; } @@ -180,6 +195,14 @@ class Context { return this._userChallengeFactory; } + public getTeamChallengeRepository():TeamChallengeRepository { + return this._teamChallengeRepository; + } + + public getTeamChallengeFactory():TeamChallengeFactory { + return this._teamChallengeFactory; + } + public getUserRepository():UserRepository { return this._userRepository; } diff --git a/backend/src/Server.ts b/backend/src/Server.ts index ee09a7e..fa0f02c 100644 --- a/backend/src/Server.ts +++ b/backend/src/Server.ts @@ -7,7 +7,6 @@ var http:any = require("http"); var express:any = require("express"); var bodyParser:any = require("body-parser"); -var session = require('client-sessions'); import UserRepository = require('./user/UserRepository'); diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 328c704..67f73c0 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -1,12 +1,19 @@ import RouterItf = require('./RouterItf'); -import ChallengeRepository = require('../challenge/UserChallengeRepository'); -import ChallengeFactory = require('../challenge/UserChallengeFactory'); +import UserChallengeRepository = require('../challenge/UserChallengeRepository'); +import UserChallengeFactory = require('../challenge/UserChallengeFactory'); + +import TeamChallengeRepository = require('../challenge/TeamChallengeRepository'); +import TeamChallengeFactory = require('../challenge/TeamChallengeFactory'); + import GoalRepository = require('../goal/GoalRepository'); import BadgeRepository = require('../badge/BadgeRepository'); import UserRepository = require('../user/UserRepository'); import TeamRepository = require('../user/TeamRepository'); -import Challenge = require('../challenge/UserChallenge'); + +import UserChallenge = require('../challenge/UserChallenge'); +import TeamChallenge = require('../challenge/TeamChallenge'); + import Clock = require('../Clock'); import ChallengeStatus = require('../Status'); import Goal = require('../goal/Goal'); @@ -27,8 +34,11 @@ class DashboardRouter extends RouterItf { private jsonStub:any = {}; private pathToStubFile:string; - private challengeRepository:ChallengeRepository; - private challengeFactory:ChallengeFactory; + private userChallengeRepository:UserChallengeRepository; + private userChallengeFactory:UserChallengeFactory; + + private teamChallengeRepository:TeamChallengeRepository; + private teamChallengeFactory:TeamChallengeFactory; private goalRepository:GoalRepository; @@ -40,9 +50,6 @@ class DashboardRouter extends RouterItf { private middleware:Middleware; - // TODO DELETE THIS TOKEN WHEN SESSION WILL BE ESTABLISHED - private currentUser:Entity; - constructor(context:Context, middleware:Middleware) { super(); @@ -52,11 +59,19 @@ class DashboardRouter extends RouterItf { var data = fs.readFileSync(this.pathToStubFile, "utf-8"); this.jsonStub = JSON.parse(data); - this.challengeRepository = context.getUserChallengeRepository(); - this.challengeFactory = context.getUserChallengeFactory(); + console.log("STUB", JSON.stringify(this.jsonStub)); + + this.userChallengeRepository = context.getUserChallengeRepository(); + this.userChallengeFactory = context.getUserChallengeFactory(); + + this.teamChallengeRepository = context.getTeamChallengeRepository(); + this.teamChallengeFactory = context.getTeamChallengeFactory(); + this.goalRepository = context.getGoalRepository(); + this.userRepository = context.getUserRepository(); this.teamRepository = context.getTeamRepository(); + this.badgeRepository = context.getBadgeRepository(); this.middleware = middleware; @@ -74,64 +89,82 @@ class DashboardRouter extends RouterItf { var result:any = {}; - var currentUser:User = self.userRepository.getUser(userID); + var currentUser:User = self.checkUserID(userID); - var team:Team = self.teamRepository.getTeam(dashboardWanted); - - if (currentUser == null && team == null) { - res.status(404).send('Euh ... Juste, tu n\'existes pas. Désolé. Bisous. Dégage'); + if (currentUser == null) { + res.status(404).send({error: 'Le profil utilisateur n\'existe pas'}); return; } + // User dashboard wanted - if (currentUser != null && team == null) { + if (dashboardWanted == 'personal') { result = self.getPersonalDashboard(currentUser); + + // Build dashboardList, every views possible for current user + var dashboardList:any[] = []; + + var teams = self.teamRepository.getTeamsByMember(currentUser.getUUID()); + + for (var teamIndex in teams) { + var currentTeam = teams[teamIndex]; + var teamDescription:any = {}; + teamDescription.id = currentTeam.getUUID(); + teamDescription.name = currentTeam.getName(); + dashboardList.push(teamDescription); + } + + result.dashboardList = dashboardList; } // Team dashboard wanted - else if (currentUser != null && team != null) { - //TODO Check if user is leader or member of team + else { + + var team:Team = self.teamRepository.getTeam(dashboardWanted); + if (team == null) { + res.status(404).send({error: 'Le dashboard demandé n\'existe pas'}); + return; + } + console.log("Dashboard de la team", team.getName(), "désiré"); var currentUserIsLeaderOfTargetedTeam = team.hasLeader(currentUser.getUUID()); if (currentUserIsLeaderOfTargetedTeam) { + console.log("L'utilisateur est leader de la team", team.getName()); result = self.getTeamDashboardForALeader(team); } else { + console.log("L'utilisateur n'est que membre de la team", team.getName()); result = self.getTeamDashboardForAMember(team); } - } - // TODO extract method - // Build dashboardList, every views possible for current user - var dashboardList:any[] = []; - var teams = self.teamRepository.getTeamsByMember(currentUser.getUUID()); + // Build dashboardList, every views possible for current user + var dashboardList:any[] = []; + + var teams = self.teamRepository.getTeamsByMember(currentUser.getUUID()); + + for (var teamIndex in teams) { + var currentTeam = teams[teamIndex]; + var teamDescription:any = {}; + teamDescription.id = currentTeam.getUUID(); + teamDescription.name = currentTeam.getName(); + dashboardList.push(teamDescription); + } - for (var teamIndex in teams) { - var currentTeam = teams[teamIndex]; - var teamDescription:any = {}; - teamDescription.id = currentTeam.getUUID(); - teamDescription.name = currentTeam.getName(); - dashboardList.push(teamDescription); + result.dashboardList = dashboardList; } - result.dashboardList = dashboardList; res.send({data: result}); }); - this.router.get('/', function (req, res) { - console.log("Getting dashboard"); - // TODO redirect login page - self.getDashboard(req, res); - }); this.router.delete('/delete/:id/:challengeID', function (req, res) { self.deleteChallenge(req, res); }); - this.router.post('/takeGoal', function (req, res) { + this.router.post('/takeGoal/', function (req, res) { self.newGoalInstance(req, res); }); @@ -155,35 +188,47 @@ class DashboardRouter extends RouterItf { valueDesc.date = Clock.getMoment(Clock.getNow()).valueOf(); valueDesc.value = value; - // console.log("DATE DU STUB AJOUTE", valueDesc.date); - - var oldJson:any[] = this.jsonStub[key].values; + var oldJson:any[] = this.jsonStub[key]; oldJson.push(valueDesc); - this.jsonStub[key].values = oldJson; + this.jsonStub[key] = oldJson; res.send('Valeur' + JSON.stringify(valueDesc) + " ajoutee au stub !"); } - setNow(req, res) { var data = req.body; + var newNow:moment.Moment = Clock.getMomentFromString(data.now); - console.log("Mise a jour de la date actuelle. Nous sommes maintenant le", newNow.date()); - Clock.setNow(newNow.valueOf()); - res.send("New 'now' : " + newNow.date()); + console.log("Mise a jour de la date actuelle. Nous sommes maintenant le", newNow.toISOString()); + Clock.setNowByString(newNow.toISOString()); + res.send("New 'now' : " + newNow.toISOString()); } newGoalInstance(req:any, res:any) { - var goalID = req.body.id; + console.log(req.body); + + var goalID = req.body.goalID; if (!goalID) { res.status(400).send({'error': 'goalID field is missing in request'}); } - var currentUser = req.params.id; + //TODO check if it's a user taking a challenge for himself + // or a team leader taking a challenge for its team - var newChallenge:Challenge = this.createChallenge(currentUser, goalID, Clock.getMoment(Clock.getNow())); + var currentUserID = req.body.userID; + var currentUser:User = this.checkUserID(currentUserID); + + var target = req.body.target; + + var newChallenge = null; + if (target == 'personal') { + newChallenge = this.createUserChallenge(currentUserID, goalID, Clock.getMoment(Clock.getNow())); + } + else { + newChallenge = this.createTeamChallenge(target, goalID, Clock.getMoment(Clock.getNow())); + } if (newChallenge == null) { res.send({'error': 'Can not take this challenge'}); @@ -194,24 +239,28 @@ class DashboardRouter extends RouterItf { } deleteChallenge(req:any, res:any) { + var challengeID = req.params.challengeID; var userID = req.params.id; var user:User = null; try { - user = this.checkUserProfile(userID); + user = this.checkUserID(userID); } - catch(e) { - if(e instanceof BadRequestException) { - res.status(400).send({error:e.getMessage()}); + catch (e) { + if (e instanceof BadRequestException) { + res.status(400).send({error: e.getMessage()}); + return; } - else if(e instanceof BadArgumentException) { - res.status(400).send({error:e.getMessage()}); + else if (e instanceof BadArgumentException) { + res.status(400).send({error: e.getMessage()}); + return; } else { - res.status(500).send({error:e.getMessage()}); + res.status(500).send({error: e.getMessage()}); + return; } } @@ -224,74 +273,22 @@ class DashboardRouter extends RouterItf { } } - getDashboard(req, res) { - console.log("\n=======================================================================\n---> Getting Dashboard\n"); - - // TODO replace getCurrentUser by session user - - var result:any = {}; - - try { - // Build dashboardList, every views possible for current user - var dashboardList:any[] = []; - - var teams = this.teamRepository.getTeamsByMember(this.currentUser.getUUID()); - for (var teamIndex in teams) { - var team = teams[teamIndex]; - var teamDescription:any = {}; - teamDescription.id = team.getUUID(); - teamDescription.name = team.getName(); - dashboardList.push(teamDescription); - } - - console.log("Dashboard views available : ", dashboardList); - - var typeOfDashboardAsked:string = req.query.typeOfDashboard; - - console.log("Dashboard view asked : ", typeOfDashboardAsked); - - if (typeOfDashboardAsked == undefined || typeOfDashboardAsked === 'personal') { - result = this.getPersonalDashboard(null); - } - else { - var teamDescriptionWanted = this.teamRepository.getTeam(typeOfDashboardAsked); - console.log("Team dashboard mode, with team : \n\t", team.getStringDescription()); - - // TODO DELETE THIS TOKEN WHEN SESSION WILL BE ESTABLISHED - // this.currentUser = teamDescriptionWanted; - - // Check if current user is leader of the team - var currentUserIsLeaderOfTargetedTeam = teamDescriptionWanted.hasLeader(this.currentUser.getUUID()); - if (currentUserIsLeaderOfTargetedTeam) { - result = this.getTeamDashboardForALeader(teamDescriptionWanted); - } - else { - result = this.getTeamDashboardForAMember(teamDescriptionWanted); - } - } - result.dashboardList = dashboardList; - - res.send({success: 'Everything is fine', data: result}); - console.log("\nSending ... \n", JSON.stringify(result)); - console.log("=======================================================================\n\n"); - - } - catch (e) { - res.send({error: e.toString()}); - } - } - getTeamDashboardForAMember(teamDescriptionWanted:Team):any { var result:any = {}; // Evaluate challenge and return them // Done before everything to be up to date this.evaluateChallengeForGivenTeam(teamDescriptionWanted); + + // First col : available goal + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.userChallengeRepository); + // Second col : badge description - var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(teamDescriptionWanted); + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenTeam(teamDescriptionWanted); // Third col : Build the description of updated challenges (potential additions/deletions) - var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(teamDescriptionWanted); + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenTeam(teamDescriptionWanted); + result.goals = {'canTakeGoal': false, goalsData: descriptionOfAvailableGoals}; result.badges = descriptionOfBadges; result.challenges = descriptionOfChallenges; @@ -308,16 +305,16 @@ class DashboardRouter extends RouterItf { this.evaluateChallengeForGivenTeam(teamDescriptionWanted); // First col : available goal - var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.challengeRepository); + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.teamChallengeRepository); // Second col : badge description - var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenEntity(teamDescriptionWanted); + var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenTeam(teamDescriptionWanted); // Third col : Build the description of updated challenges (potential additions/deletions) - var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenEntity(teamDescriptionWanted); + var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenTeam(teamDescriptionWanted); // Build the response - result.goals = descriptionOfAvailableGoals; + result.goals = {'canTakeGoal': true, goalsData: descriptionOfAvailableGoals}; result.badges = descriptionOfBadges; result.challenges = descriptionOfChallenges; @@ -325,19 +322,18 @@ class DashboardRouter extends RouterItf { } getPersonalDashboard(user:User):any { - var result:any = {}; + console.log("\t Personal Dashboard mode"); + console.log('\t Current client', user.getName()); - console.log('Current client', user.getName()); - - console.log("Personal Dashboard mode"); + var result:any = {}; // Evaluate challenge and return them // Done before everything to be up to date this.evaluateChallengesForGivenUser(user); // First col : available goal - var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(user, this.challengeRepository); + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(user, this.userChallengeRepository); // Second col : badge description var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenUser(user); @@ -346,20 +342,20 @@ class DashboardRouter extends RouterItf { var descriptionOfChallenges:any[] = this.buildCurrentChallengesDescriptionForGivenUser(user); // Build the response - result.goals = descriptionOfAvailableGoals; + result.goals = {'canTakeGoal': true, goalsData: descriptionOfAvailableGoals}; result.badges = descriptionOfBadges; result.challenges = descriptionOfChallenges; return result; } - private buildCurrentChallengesDescriptionForGivenEntity(team:Team):any[] { + private buildCurrentChallengesDescriptionForGivenTeam(team:Team):any[] { var descriptionOfChallenges:any[] = []; var challenges = team.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); - var currentChallengeDesc = currentChallenge.getDataInJSON(); + var currentChallenge = this.teamChallengeRepository.getChallengeByID(currentChallengeID); + var currentChallengeDesc = currentChallenge.getDataForClient(); descriptionOfChallenges.push(currentChallengeDesc); } @@ -372,18 +368,18 @@ class DashboardRouter extends RouterItf { var challenges = user.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); - var currentChallengeDesc = currentChallenge.getDataInJSON(); + var currentChallenge = this.userChallengeRepository.getChallengeByID(currentChallengeID); + var currentChallengeDesc = currentChallenge.getDataForClient(); descriptionOfChallenges.push(currentChallengeDesc); } return descriptionOfChallenges; } - private buildBadgesDescriptionForGivenEntity(team:Team):any[] { + private buildBadgesDescriptionForGivenTeam(team:Team):any[] { var descriptionOfBadges:any[] = []; - var badges = team.getBadgesID(); + var badges = team.getBadges(); for (var currentBadgeIDIndex in badges) { var currentBadge = this.badgeRepository.getBadge(currentBadgeIDIndex).getData(); @@ -420,9 +416,9 @@ class DashboardRouter extends RouterItf { var challenges = team.getCurrentChallenges(); for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); + var currentChallenge = this.teamChallengeRepository.getChallengeByID(currentChallengeID); - this.evaluateChallenge(team, currentChallenge, currentChallengeID); + this.evaluateTeamChallenge(team, currentChallenge, currentChallengeID); } } @@ -431,13 +427,13 @@ class DashboardRouter extends RouterItf { for (var challengeIndex in challenges) { var currentChallengeID = challenges[challengeIndex]; - var currentChallenge = this.challengeRepository.getChallengeByID(currentChallengeID); + var currentChallenge = this.userChallengeRepository.getChallengeByID(currentChallengeID); this.evaluateChallenge(user, currentChallenge, currentChallengeID); } } - private evaluateChallenge(entity, challengeToEvaluate:Challenge, challengeID) { + private evaluateTeamChallenge(entity, challengeToEvaluate:TeamChallenge, challengeID) { var self = this; if (!DashboardRouter.DEMO) { @@ -460,10 +456,10 @@ class DashboardRouter extends RouterItf { function () { var result = challengeToEvaluate.evaluate(required); if (result) { - var newChall = self.createChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChall = self.createUserChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); this.addBadge(challengeID, entity.getUUID()); if (newChall != null) { - self.evaluateChallenge(entity, newChall, newChall.getId()); + self.evaluateChallenge(entity, newChall, newChall.getID()); } } console.log("All data were retrieve properly"); @@ -484,14 +480,97 @@ class DashboardRouter extends RouterItf { challengeToEvaluate.setStatus(ChallengeStatus.RUN); } - for(var currentConditionID in challengeToEvaluate.getSensors()) { - var currentConditionDesc = challengeToEvaluate.getSensors()[currentConditionID]; - currentConditionDesc = merge(currentConditionDesc, this.jsonStub); + + var result:any = challengeToEvaluate.evaluate(this.jsonStub); + + + console.log("RESULT", result); + + // Check if the challenge is achieved and finished + if (result.achieved && result.finished) { + console.log("Le challenge est réussi et terminé"); + + // Add finished badge to current user + this.addFinishedBadge(challengeID, entity.getUUID()); + + /* + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); + } + */ + } + + // Check if the challenge is not achieved but finished + else if (!result.achieved && result.finished) { + console.log("Le challenge est FAIL et terminé"); + + entity.deleteChallenge(challengeToEvaluate.getID()); + + /* + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); + } + */ } - var evaluateData = challengeToEvaluate.getSensors(); + return challengeToEvaluate; + } + } + + + private evaluateChallenge(entity, challengeToEvaluate:UserChallenge, challengeID) { + var self = this; + + if (!DashboardRouter.DEMO) { + + //TODO move what follow + var required = challengeToEvaluate.getSensors(); - var result:any = challengeToEvaluate.evaluate(evaluateData); + var requiredSensorName = Object.keys(required); + var numberToLoad:number = requiredSensorName.length; + + for (var currentSensorName in requiredSensorName) { + (function (currentSensorName) { + var startDate:string = '' + required[currentSensorName].startDate; + var endDate:string = '' + required[currentSensorName].endDate; + + var path = 'http://smartcampus.unice.fr/sensors/' + currentSensorName + '/data?date=' + startDate + '/' + endDate; + var dataJsonString = ""; + + this.middleware.getSensorsInfo(required, numberToLoad, dataJsonString, path, + function () { + var result = challengeToEvaluate.evaluate(required); + if (result) { + var newChall = self.createUserChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + this.addBadge(challengeID, entity.getUUID()); + if (newChall != null) { + self.evaluateChallenge(entity, newChall, newChall.getID()); + } + } + console.log("All data were retrieve properly"); + return challengeToEvaluate; + }, + function () { + return {error: "Error occurred in middleware"}; + }); + + })(requiredSensorName[currentSensorName]); + } + } + else { + console.log('++++++++++++++++++++++ \tMODE DEMO\t+++++++++++++++++++++'); + + if (challengeToEvaluate.haveToStart(Clock.getCurrentDemoMoment())) { + console.log("Le challenge doit démarrer"); + challengeToEvaluate.setStatus(ChallengeStatus.RUN); + } + + + var result:any = challengeToEvaluate.evaluate(this.jsonStub); // Check if the challenge is achieved and finished @@ -502,9 +581,9 @@ class DashboardRouter extends RouterItf { this.addFinishedBadge(challengeID, entity.getUUID()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createUserChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); + self.evaluateChallenge(entity, newChallenge, newChallenge.getID()); } } @@ -512,12 +591,12 @@ class DashboardRouter extends RouterItf { else if (challengeToEvaluate.getStatus() == ChallengeStatus.FAIL) { console.log("Le challenge est FAIL et terminé"); - entity.deleteChallenge(challengeToEvaluate.getId()); + entity.deleteChallenge(challengeToEvaluate.getID()); // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + var newChallenge = self.createUserChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(entity, newChallenge, newChallenge.getId()); + self.evaluateChallenge(entity, newChallenge, newChallenge.getID()); } } @@ -533,13 +612,13 @@ class DashboardRouter extends RouterItf { console.log('challenge ID : ', challengeID); */ var user = this.userRepository.getUser(userID); - var badgeID = this.challengeRepository.getBadgeByChallengeID(challengeID); + var badgeID = this.userChallengeRepository.getBadgeByChallengeID(challengeID); user.addBadge(badgeID); user.deleteChallenge(challengeID); } - createChallenge(userID:string, goalID:string, date:moment.Moment):Challenge { + createUserChallenge(userID:string, goalID:string, date:moment.Moment):UserChallenge { var user:User = this.userRepository.getUser(userID); var goal:Goal = this.goalRepository.getGoal(goalID); @@ -550,10 +629,38 @@ class DashboardRouter extends RouterItf { return null; } - this.challengeRepository.addGoalInstance(newChallenge); + this.userChallengeRepository.addUserChallenge(newChallenge); return newChallenge; } + createTeamChallenge(teamID:string, goalID:string, date:moment.Moment) { + + var team:Team = this.teamRepository.getTeam(teamID); + var goal:Goal = this.goalRepository.getGoal(goalID); + + + + var newChallenge = team.addChallenge(goal,this.userChallengeRepository, date); + + if (newChallenge == null) { + return null; + } + + this.teamChallengeRepository.addTeamChallenge(newChallenge); + return newChallenge; + } + + + checkUserID(userID):User { + + var currentUser:User = this.userRepository.getUser(userID); + + if (currentUser == null) { + throw new BadArgumentException('Given username can not be found'); + } + + return currentUser; + } checkUserProfile(username):User { diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index 2c2c914..5f83e33 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -1,6 +1,12 @@ +/// +/// + import uuid = require('node-uuid'); var merge = require('merge'); +var moment = require('moment'); +var moment_timezone = require('moment-timezone'); + import Status= require('../Status'); import UserChallenge= require('./UserChallenge'); import Team= require('../user/Team'); @@ -14,7 +20,11 @@ class TeamChallenge { private childChallenges:UserChallenge[]; private status:Status; - private elapsedTime:number; + + private startDate:moment.Moment; + private endDate:moment.Moment; + + private durationAchieved:number; private progress:any = {}; constructor(team:Team, childChallenges:UserChallenge[], id = null) { @@ -26,40 +36,138 @@ class TeamChallenge { this.team = team; this.childChallenges = childChallenges; - this.status = this.childChallenges[0].getStatus(); + this.status = this.getStatus(); + this.checkChildrenTimeBoxes(); + } + + getID():string { + return this.id; + } + hasUUID(uuid:string) { + return this.id == uuid; + } + + getName():string { + return this.childChallenges[0].getName(); + } + + getBadge() { + return this.childChallenges[0].getBadge(); + } + + setStatus(status) { + this.status = status; + } + + getGoal() { + return this.childChallenges[0].getGoal(); + } + + getStartDate() { + return this.childChallenges[0].getStartDate(); + } + + getEndDate() { + return this.childChallenges[0].getEndDate(); + } + + haveToStart(now):boolean { + return this.childChallenges[0].haveToStart(now); + } + + getStatus():Status { + var succeed:boolean = false; + var stillRunning:boolean = false; + + for (var currentChildIndex in this.childChallenges) { + var currentChild = this.childChallenges[currentChildIndex]; + var currentChildStatus = currentChild.getStatus(); + + console.log("Current child status", currentChildStatus); + + stillRunning = stillRunning && (currentChildStatus == Status.RUN); + if (stillRunning) { + return Status.RUN; + } + + succeed = succeed && (currentChildStatus == Status.SUCCESS); + if (!succeed) { + return Status.FAIL; + } + } + + return Status.SUCCESS; + } + + checkChildrenTimeBoxes() { + var startDate:moment.Moment = this.childChallenges[0].getStartDate(); + var endDate:moment.Moment = this.childChallenges[0].getEndDate(); + + for (var currentChildIndex in this.childChallenges) { + var currentChild = this.childChallenges[currentChildIndex]; + + var startDateOfCurrentChild = currentChild.getStartDate(); + if (startDate.toISOString() != startDateOfCurrentChild.toISOString()) { + throw new BadArgumentException('Can not build team challenge ! Some child challenge does not have the same start date as the other'); + } + + var endDateOfCurrentChild = currentChild.getEndDate(); + if (endDate.toISOString() != endDateOfCurrentChild.toISOString()) { + throw new BadArgumentException('Can not build team challenge ! Some child challenge does not have the same end date as the other'); + } + } + + this.startDate = startDate; + this.endDate = endDate; } evaluate(data):void { var childProgress:number = 0; + var achieved:boolean = false; + var finished:boolean = false; + + var childProgressDescription:any[] = []; for (var currentChildIndex in this.childChallenges) { var currentChild:UserChallenge = this.childChallenges[currentChildIndex]; - currentChild.evaluate(data); - var currentChildGlobalProgression:number = currentChild.getGlobalProgression(); + + var childResult = currentChild.evaluate(data); + + achieved = achieved && currentChild.getStatus() == Status.SUCCESS; + + var currentChildGlobalProgression:number = childResult.percentageAchieved; childProgress += currentChildGlobalProgression; var currentChildProgressionDescription:any = { - name: currentChild.getName(), - progression: currentChildGlobalProgression + description: currentChild.getUser().getName(), + percentageAchieved: currentChildGlobalProgression, + achieved: childResult.achieved }; - this.progress[currentChild.getId()] = currentChildProgressionDescription; + childProgressDescription.push(currentChildProgressionDescription); } - this.elapsedTime = this.childChallenges[0].getTimeProgress(); + this.durationAchieved = this.childChallenges[0].getTimeProgress(); + this.progress['percentageAchieved'] = childProgress / this.childChallenges.length; + this.progress['durationAchieved'] = this.durationAchieved; + this.progress['finished'] = this.durationAchieved == 100; + this.progress['achieved'] = achieved; + this.progress['conditions'] = childProgressDescription; + + return this.progress; } getSensors() { /* - Precisions : - We can not assume that a specific sensor is bound to a specific user - since two users can share an office. - But we can assume that for a given challenge, sensor of each user - have the same timeBox and will be identical. - Because of this, we can simply merge required sensors and not - try to merge different timeBoxes or whatever. + Precisions : + We can not assume that a specific sensor is bound to a specific user + since two users can share an office. + But we can assume that for a given challenge, sensor of each user + have the same timeBox and will be identical. + Because of this, we can simply merge required sensors and not + try to merge different timeBoxes or whatever. */ var result:any = {}; @@ -71,6 +179,37 @@ class TeamChallenge { return result; } + + getDataInJSON():any { + var childrenIDs:string[] = []; + for (var currentChildrenIndex in this.childChallenges) { + var currentChild:UserChallenge = this.childChallenges[currentChildrenIndex]; + + var currentChildID:string = currentChild.getID(); + childrenIDs.push(currentChildID); + } + + return { + id: this.id, + team: this.team.getUUID(), + children: childrenIDs + } + + } + + + getDataForClient():any { + return { + id: this.id, + type: 'team', + startDate: this.startDate, + endDate: this.endDate, + goal: this.childChallenges[0].getGoal().getName(), + user: this.getName(), + progress: this.progress, + status: this.status + } + } } export = TeamChallenge; \ No newline at end of file diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts index e29faec..d5d7512 100644 --- a/backend/src/challenge/TeamChallengeFactory.ts +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -3,18 +3,23 @@ import User = require('../user/User'); import Goal = require('../goal/Goal'); import UserChallenge = require('../challenge/UserChallenge'); import TeamChallenge = require('../challenge/TeamChallenge'); +import UserChallengeRepository = require('../challenge/UserChallengeRepository'); class TeamChallengeFactory { - createTeamChallenge(team:Team, goal:Goal, now) { + createTeamChallenge(team:Team, goal:Goal, userChallengeRepository:UserChallengeRepository, now) { var membersChallenges:UserChallenge[] = []; var members:User[] = team.getMembers(); + console.log("Team's members", team.getStringDescriptionOfMembers()); + for(var currentMemberIndex in members) { var currentMember:User = members[currentMemberIndex]; var currentUserChallenge:UserChallenge = currentMember.addChallenge(goal, now); + userChallengeRepository.addUserChallenge(currentUserChallenge); membersChallenges.push(currentUserChallenge); + console.log("Le user", currentMember.getName(), "has this challenge added", currentUserChallenge.getID()); } return new TeamChallenge(team, membersChallenges); diff --git a/backend/src/challenge/TeamChallengeRepository.ts b/backend/src/challenge/TeamChallengeRepository.ts index 08cf7a0..fd76a71 100644 --- a/backend/src/challenge/TeamChallengeRepository.ts +++ b/backend/src/challenge/TeamChallengeRepository.ts @@ -1,3 +1,58 @@ -/** - * Created by Benjamin on 28/08/2015. - */ + +import TeamChallenge = require('./TeamChallenge'); +import TeamChallengeFactory = require('./TeamChallengeFactory'); +import GoalRepository = require('../goal/GoalRepository'); +import UserRepository = require('../user/UserRepository'); +import Badge = require('../badge/Badge'); + + +class TeamChallengeRepository { + + private teamChallengesArray:TeamChallenge[] = []; + private factory:TeamChallengeFactory; + + constructor() { + this.factory = new TeamChallengeFactory(); + } + + + addTeamChallenge(aChallenge:TeamChallenge) { + this.teamChallengesArray.push(aChallenge); + } + + getChallengeByID(challengeID:string):TeamChallenge { + for (var i in this.teamChallengesArray) { + var currentBadge = this.teamChallengesArray[i]; + if (currentBadge.hasUUID(challengeID)) { + return currentBadge; + } + } + + return null; + } + + getBadgeByChallengeID(challengeID:string):string { + return this.getChallengeByID(challengeID).getBadge(); + } + + displayShortState() { + console.log("\n\n+++\t Etat du repository des defis\t+++"); + + for (var currentChallengeIndex in this.teamChallengesArray) { + var currentChallenge = this.teamChallengesArray[currentChallengeIndex]; + console.log("#", currentChallenge.getID(), "\t |\tDefi : '", currentChallenge.getName(), "'") + } + } + + getDataInJSON():any { + var result:any[] = []; + + for (var currentChallengeIndex in this.teamChallengesArray) { + result.push(this.teamChallengesArray[currentChallengeIndex].getDataInJSON()); + } + + return result; + } +} + +export = TeamChallengeRepository; \ No newline at end of file diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 023911d..925c103 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -96,7 +96,7 @@ class UserChallenge { return this.goal.getName(); } - getId():string { + getID():string { return this.id; } @@ -118,6 +118,56 @@ class UserChallenge { } getSensors():any { + + var required:any = {}; + + for (var conditionID in this.mapConditionIDToSensorAndTimeBoxRequired) { + var currentConditionDescription = this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; + var sensorsOfCurrentCondition = currentConditionDescription.sensors; + + for(var currentSensorIndex in sensorsOfCurrentCondition) { + var currentSensor:string = sensorsOfCurrentCondition[currentSensorIndex]; + + if(required[currentSensor] == null) { + required[currentSensor] = currentConditionDescription.timeBox; + } + else { + var oldTimeBox = required[currentSensor]; + var oldStartDate:moment.Moment = oldTimeBox.start; + var oldEndDate:moment.Moment = oldTimeBox.end; + + var currentTimeBox = currentConditionDescription.timeBox; + var currentStartDate:moment.Moment = currentTimeBox.start; + var currentEndDate:moment.Moment = currentTimeBox.end; + + var newStartDate:moment.Moment = null; + var newEndDate:moment.Moment = null; + + if(currentStartDate.isBefore(oldStartDate)) { + newStartDate = currentStartDate; + } + else { + newStartDate = oldStartDate; + } + + if(currentEndDate.isAfter(oldEndDate)) { + newEndDate = currentEndDate; + } + else { + newEndDate = oldEndDate; + } + + var newTimeBox:any = { + start:newStartDate, + end:newEndDate + }; + + required[currentSensor] = newTimeBox; + } + } + + } + return this.mapConditionIDToSensorAndTimeBoxRequired; } @@ -150,30 +200,42 @@ class UserChallenge { return now.isAfter(this.startDate) && now.isBefore(this.endDate); } + retrieveSymbolicNameFromSensor(sensorName:string):string { + for(var currentSymbolicName in this.mapSymbolicNameToSensor) { + + var sensorNameBound = this.mapSymbolicNameToSensor[currentSymbolicName]; + + if(sensorNameBound == sensorName) { + return currentSymbolicName; + } + } + + return null; + } + /** * * @param values * { - * : - * { - * symbolicNames:[...], - * : [ ] - * } + * : [ { date:"time in millis", value:__}, ... ] * } * @returns {any} */ evaluate(values:any):any { - for(var currentConditionID in values) { - var currentConditionDesc = values[currentConditionID]; - currentConditionDesc['values'] = {}; - for(var currentSymbolicNameIndex in currentConditionDesc.symbolicNames) { - var currentSymbolicName = currentConditionDesc.symbolicNames[currentSymbolicNameIndex]; - var sensorNameBound = this.mapSymbolicNameToSensor[currentSymbolicName]; - if(sensorNameBound == null) continue; - var dataForCurrentSensor = currentConditionDesc[sensorNameBound]; - currentConditionDesc['values'][currentSymbolicName] = dataForCurrentSensor; + + for (var conditionID in this.mapConditionIDToSensorAndTimeBoxRequired) { + var currentConditionDescription = this.mapConditionIDToSensorAndTimeBoxRequired[conditionID]; + + currentConditionDescription['values'] = {}; + + var sensorsRequired = currentConditionDescription.sensors; + for(var currentSensorRequiredIndex in sensorsRequired) { + var currentSensorRequired = sensorsRequired[currentSensorRequiredIndex]; + var symbolicNameBound = this.retrieveSymbolicNameFromSensor(currentSensorRequired); + currentConditionDescription['values'][symbolicNameBound] = values[currentSensorRequired]; } + } // Check if badge is running. If Waiting or failed, it must be left unchanged @@ -181,7 +243,8 @@ class UserChallenge { return false; } - var resultEval = this.goal.evaluate(values, this); + var resultEval = this.goal.evaluate(this.mapConditionIDToSensorAndTimeBoxRequired, this); + var durationAchieved:number = this.updateDurationAchieved(Clock.getNow()); resultEval['durationAchieved'] = durationAchieved; @@ -204,6 +267,7 @@ class UserChallenge { } this.progress = resultEval; + console.log('Résultat de l\'évaluation du challenge : achieved', resultEval.achieved, 'finished', resultEval.finished); return resultEval; } @@ -235,7 +299,21 @@ class UserChallenge { startDate: this.startDate, endDate: this.endDate, goal: this.goal.getUUID(), - user: this.user.getUUID() + user: this.user.getUUID(), + progress: this.progress + } + } + + getDataForClient():any { + return { + id: this.id, + type:'user', + startDate: this.startDate, + endDate: this.endDate, + goal: this.goal.getName(), + user: this.user.getName(), + progress: this.progress, + status:this.status } } diff --git a/backend/src/challenge/UserChallengeRepository.ts b/backend/src/challenge/UserChallengeRepository.ts index 8e35dcd..e718907 100644 --- a/backend/src/challenge/UserChallengeRepository.ts +++ b/backend/src/challenge/UserChallengeRepository.ts @@ -1,159 +1,57 @@ -import Challenge = require('./UserChallenge'); -import ChallengeFactory = require('./UserChallengeFactory'); +import UserChallenge = require('./UserChallenge'); +import UserChallengeFactory = require('./UserChallengeFactory'); import GoalRepository = require('../goal/GoalRepository'); -import UserProvider = require('../user/UserRepository'); +import UserRepository = require('../user/UserRepository'); import Badge = require('../badge/Badge'); -class BadgeProvider { +class UserChallengeRepository { - private goalInstancesArray:Challenge[] = []; - - private factory:ChallengeFactory; + private userChallengesArray:UserChallenge[] = []; + private factory:UserChallengeFactory; constructor() { - this.factory = new ChallengeFactory(); - } - - public displayShortState() { - console.log("\n\n+++\t Etat du repository des defis\t+++"); - - for(var currentChallengeIndex in this.goalInstancesArray) { - var currentChallenge = this.goalInstancesArray[currentChallengeIndex]; - console.log("#",currentChallenge.getId(),"\t |\tDefi : '", currentChallenge.getName(), "'") - } + this.factory = new UserChallengeFactory(); } - public getBadgeByChallengeID(challengeID:string):string { - return this.getChallengeByID(challengeID).getBadge(); - } - public addGoalInstance(aChallenge:Challenge) { - this.goalInstancesArray.push(aChallenge); + addUserChallenge(aChallenge:UserChallenge) { + this.userChallengesArray.push(aChallenge); } - public getChallengeByID(challengeID:string):Challenge { - for (var i in this.goalInstancesArray) { - var currentBadge = this.goalInstancesArray[i]; - if (currentBadge.hasUUID(challengeID)) { - return currentBadge; + getChallengeByID(challengeID:string):UserChallenge { + for (var i in this.userChallengesArray) { + var currentChallenge = this.userChallengesArray[i]; + if (currentChallenge.hasUUID(challengeID)) { + return currentChallenge; } } return null; } - public getListOfGoalInstancesInJsonFormat():any[] { - var result = []; - for (var i in this.goalInstancesArray) { - var currentGoalInstanceDesc:any = {}; - currentGoalInstanceDesc.name = this.goalInstancesArray[i].getName(); - currentGoalInstanceDesc.id = this.goalInstancesArray[i].getId(); - result.push(currentGoalInstanceDesc); - } - - return result; - } - - public getGoalInstancesDescriptionInJsonFormat(datastub:any = null):any[] { - var result = []; - - for (var i in this.goalInstancesArray) { - - this.goalInstancesArray[i].evaluate(datastub); - - var currentBadgeDesc:any = {}; - currentBadgeDesc.name = this.goalInstancesArray[i].getName(); - currentBadgeDesc.id = this.goalInstancesArray[i].getId(); - currentBadgeDesc.desc = this.goalInstancesArray[i].getGoal().getName(); - currentBadgeDesc.progress = this.goalInstancesArray[i].getGlobalProgression(); - - currentBadgeDesc.startDate = this.goalInstancesArray[i].getStartDate(); - currentBadgeDesc.endDate = this.goalInstancesArray[i].getEndDate(); - - currentBadgeDesc.timeProgress = this.goalInstancesArray[i].getTimeProgress(); - - - var statusDesc:string = ''; - var badgeStatus:number = this.goalInstancesArray[i].getStatus(); - - statusDesc = this.getBadgeStatus(badgeStatus); - - currentBadgeDesc.status = statusDesc; - if (currentBadgeDesc.status != 'SUCCESS') { - result.push(currentBadgeDesc); - } - } - - return result; - } - - public getFinishedGoalInstances():Challenge[] { - console.log('get finished goal'); - var goalFinished:Challenge[] = []; - for (var i in this.goalInstancesArray) { - var statusDesc = this.getBadgeStatus(this.goalInstancesArray[i].getStatus()); - if (statusDesc === 'SUCCESS') { - console.log('---Find one', this.goalInstancesArray[i]); - goalFinished.push(this.goalInstancesArray[i]); - this.goalInstancesArray.splice(i, 1); - } - } - - return goalFinished; - } - - public removeUselessGoalInstances() { - for (var i in this.goalInstancesArray) { - var statusDesc = this.getBadgeStatus(this.goalInstancesArray[i].getStatus()); - if (statusDesc === 'SUCCESS') { - this.goalInstancesArray.splice(i, 1); - } - } + getBadgeByChallengeID(challengeID:string):string { + return this.getChallengeByID(challengeID).getBadge(); } - public removeGoalInstance(goalInstanceUuid:string) { - for (var i in this.goalInstancesArray) { - console.log('searching for the correct goal instance'); - var currentBadge = this.goalInstancesArray[i]; - if (currentBadge.hasUUID(goalInstanceUuid)) { - console.log('goal instance found'); - this.goalInstancesArray.splice(i, 1); - console.log('removed : ', this.goalInstancesArray); - break; - } - } - } + displayShortState() { + console.log("\n\n+++\t Etat du repository des defis\t+++"); - private getBadgeStatus(badgeStatus:number):string { - switch (badgeStatus) { - case 0: - return 'WAIT'; - case 1: - return 'RUNNING'; - break; - case 2: - return 'SUCCESS'; - break; - case 3: - return 'FAIL'; - break; - default: - return 'UNKNOWN'; - break; + for (var currentChallengeIndex in this.userChallengesArray) { + var currentChallenge = this.userChallengesArray[currentChallengeIndex]; + console.log("#", currentChallenge.getID(), "\t |\tDefi : '", currentChallenge.getName(), "'") } - return 'UNKNOWN' } - public getDataInJSON():any { + getDataInJSON():any { var result:any[] = []; - for(var currentChallengeIndex in this.goalInstancesArray) { - result.push(this.goalInstancesArray[currentChallengeIndex].getDataInJSON()); + for (var currentChallengeIndex in this.userChallengesArray) { + result.push(this.userChallengesArray[currentChallengeIndex].getDataInJSON()); } return result; } } -export = BadgeProvider; \ No newline at end of file +export = UserChallengeRepository; \ No newline at end of file diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index b186bc1..66754e1 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -83,9 +83,8 @@ class AverageOnValue extends Condition { percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; } - var finished:boolean = percentageAchieved === 100; - var result:any = {description: this.description, percentageAchieved: percentageAchieved, finished: finished}; - + var achieved:boolean = percentageAchieved === 100; + var result:any = {description: this.description, percentageAchieved: percentageAchieved, achieved: achieved}; return result; } @@ -95,7 +94,7 @@ class AverageOnValue extends Condition { for (var currentValueIndex in values) { var currentPairDateValue:any = values[currentValueIndex]; - var currentMoment:moment.Moment = Clock.getMomentFromString(currentPairDateValue.date); + var currentMoment:moment.Moment = Clock.getMomentFromUnixTimeInMillis(parseInt(currentPairDateValue.date)); if (currentMoment.isAfter(dateOfCreation)) { newValues.push(currentPairDateValue.value); diff --git a/backend/src/condition/Condition.ts b/backend/src/condition/Condition.ts index cd52421..e10f28c 100644 --- a/backend/src/condition/Condition.ts +++ b/backend/src/condition/Condition.ts @@ -56,7 +56,7 @@ class Condition { * } * @returns {any[]} */ - keepUsefulValues(data:any, conditionDescription:any):any { + keepUsefulValues(data:any, conditionDescription:any):any { var result:any = {}; var startDate:moment.Moment = conditionDescription.timeBox.start; @@ -69,7 +69,9 @@ class Condition { var currentDataArray:any = data[currentSymbolicName]; for (var currentDataIndex in currentDataArray) { var currentData:any = currentDataArray[currentDataIndex]; - var date:moment.Moment = Clock.getMomentFromString(currentData.date); + var date:moment.Moment = Clock.getMomentFromUnixTimeInMillis(parseInt(currentData.date)); + + if (date.isAfter(startDate) && date.isBefore(endDate)) { currentResult.push(currentData); } diff --git a/backend/src/context/DemoContext.ts b/backend/src/context/DemoContext.ts deleted file mode 100644 index c6c3c7e..0000000 --- a/backend/src/context/DemoContext.ts +++ /dev/null @@ -1,20 +0,0 @@ -import UUID = require('node-uuid'); - -import Context = require('../Context'); - -import GoalRepository = require('../goal/GoalRepository'); -import ChallengeRepository = require('../challenge/UserChallengeRepository'); -import UserRepository = require('../user/UserRepository'); - -import Goal = require('../goal/Goal'); -import GoalExpression = require('../condition/expression/GoalExpression'); -import Operand = require('../condition/expression/Operand'); - - -import Challenge = require('../challenge/UserChallenge'); - -class DemoContext { - -} - -export = DemoContext; \ No newline at end of file diff --git a/backend/src/context/TestContext.ts b/backend/src/context/TestContext.ts deleted file mode 100644 index d4fd35f..0000000 --- a/backend/src/context/TestContext.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Created by Benjamin on 01/09/2015. - */ diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index edbbdfc..3da1484 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -92,11 +92,15 @@ class Goal { var achieved:boolean = true; + var conditionsDescription:any[] =[]; + for (var i = 0; i < this.conditionsArray.length; i++) { var currentCondition:Condition = this.conditionsArray[i]; var currentConditionDescription:any = data[currentCondition.getID()]; var currentConditionState = currentCondition.evaluate(currentConditionDescription.values, currentConditionDescription); + conditionsDescription.push(currentConditionState); + achieved = achieved && currentConditionState.achieved; result[currentCondition.getID()] = currentConditionState; @@ -105,6 +109,8 @@ class Goal { numberOfConditionsAchieved = (currentConditionState.achieved)?numberOfConditionsAchieved+1 : numberOfConditionsAchieved; } + result['conditions'] = conditionsDescription; + var percentageAchieved:number = (numberOfConditionsAchieved * 100) / numberOfConditions; result['percentageAchieved'] = percentageAchieved; result['achieved'] = achieved; diff --git a/backend/src/goal/GoalRepository.ts b/backend/src/goal/GoalRepository.ts index 73247f5..eeb3e35 100644 --- a/backend/src/goal/GoalRepository.ts +++ b/backend/src/goal/GoalRepository.ts @@ -1,8 +1,9 @@ import Goal = require('./Goal'); import GoalFactory = require('./GoalFactory'); import BadgeRepository = require('../badge/BadgeRepository'); -import ChallengeRepository = require('../challenge/UserChallengeRepository'); -import Challenge = require('../challenge/UserChallenge'); +import UserChallengeRepository = require('../challenge/UserChallengeRepository'); +import TeamChallengeRepository = require('../challenge/TeamChallengeRepository'); +import UserChallenge = require('../challenge/UserChallenge'); import Badge = require('../badge/Badge'); import User = require('../user/User'); import Team = require('../user/Team'); @@ -61,7 +62,7 @@ class GoalDefinitionRepository { } // TODO DELETE OR - getListOfNotTakenGoalInJSONFormat(user:User|Team, challengeRepository:ChallengeRepository) { + getListOfNotTakenGoalInJSONFormat(user:User|Team, challengeRepository:UserChallengeRepository|TeamChallengeRepository) { var result = []; var currentChallengesID:string[] = user.getCurrentChallenges(); @@ -69,7 +70,7 @@ class GoalDefinitionRepository { var takenGoals:Goal[] = []; for (var currentChallengeIDIndex in currentChallengesID) { var currentChallengeID:string = currentChallengesID[currentChallengeIDIndex]; - var currentChallenge:Challenge = challengeRepository.getChallengeByID(currentChallengeID); + var currentChallenge = challengeRepository.getChallengeByID(currentChallengeID); takenGoals.push(currentChallenge.getGoal()); } diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 82a9da9..6014467 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -6,6 +6,8 @@ import BadgeIDsToNumberOfTimesEarnedMap = require('./BadgeIDsToNumberOfTimesEarn import User = require('./User'); import Goal = require('../goal/Goal'); import TeamChallengeFactory = require('../challenge/TeamChallengeFactory'); +import TeamChallenge = require('../challenge/TeamChallenge'); +import UserChallengeRepository = require('../challenge/UserChallengeRepository'); class Team { private id; @@ -60,8 +62,8 @@ class Team { return this.currentChallenges; } - addChallenge(goal:Goal, now:moment.Moment):TeamChallengeFactory { - var newChallenge = this.challengeFactory.createTeamChallenge(this, goal, now); + addChallenge(goal:Goal, userChallengeRepository:UserChallengeRepository, now:moment.Moment):TeamChallenge { + var newChallenge = this.challengeFactory.createTeamChallenge(this, goal,userChallengeRepository, now); /*FIXME // Check if we try @@ -69,11 +71,12 @@ class Team { return null; } - this.currentChallenges.push(newChallenge.getId()); + this.currentChallenges.push(newChallenge.getID()); return newChallenge; */ - return null; + this.currentChallenges.push(newChallenge.getID()); + return newChallenge; } deleteChallenge(challengeID:string):void { @@ -143,6 +146,10 @@ class Team { return Object.keys(this.badgesMap); } + getBadges():BadgeIDsToNumberOfTimesEarnedMap { + return this.badgesMap; + } + public getDataInJSON():any { var membersIDs:any[] =[]; for(var memberIndex in this.members) { diff --git a/backend/src/user/TeamFactory.ts b/backend/src/user/TeamFactory.ts index f26d3e7..17e9031 100644 --- a/backend/src/user/TeamFactory.ts +++ b/backend/src/user/TeamFactory.ts @@ -4,7 +4,7 @@ import Entity = require('./Entity'); import Team = require('./Team'); import User = require('./User'); import UserRepository = require('./UserRepository'); - +import TeamChallengeFactory = require ('../challenge/TeamChallengeFactory'); /* if (name == null) { @@ -28,7 +28,7 @@ import UserRepository = require('./UserRepository'); } */ class TeamFactory { - public createTeam(data:any, userRepository:UserRepository):Team { + public createTeam(data:any, userRepository:UserRepository, teamChallengeFactory:TeamChallengeFactory):Team { var teamID:string = data.id; teamID = (teamID == null) ? uuid.v4() : teamID; @@ -49,8 +49,7 @@ class TeamFactory { var leaderID:string = data.leader; var leader = userRepository.getUser(leaderID); - //TODO FIX NULL - var team:Team = new Team(teamID, teamName, leader, members, currentChallenges, finishedBadgesMap, null); + var team:Team = new Team(teamID, teamName, leader, members, currentChallenges, finishedBadgesMap, teamChallengeFactory); return team; } } diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index d8c51ed..af8287e 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -91,7 +91,7 @@ class User { return null; } - this.currentChallenges.push(newChallenge.getId()); + this.currentChallenges.push(newChallenge.getID()); return newChallenge; } diff --git a/backend/stub_values.json b/backend/stub_values.json index a61b6d4..f184126 100644 --- a/backend/stub_values.json +++ b/backend/stub_values.json @@ -1,11 +1,21 @@ { "TEMP_443V": [ { - "date": "2015-08-03T12:00:00.000Z", + "date": 1440849600000, "value": 20 }, { - "date":"2015-08-04T12:00:00.000Z", + "date":1440856800000, + "value":22 + } + ], + "AC_555V": [ + { + "date": 1440849600000, + "value": 24 + }, + { + "date":1440856800000, "value":22 } ] diff --git a/backend/stub_values_test.json b/backend/stub_values_test.json index a61b6d4..f67cd8f 100644 --- a/backend/stub_values_test.json +++ b/backend/stub_values_test.json @@ -1,11 +1,11 @@ { "TEMP_443V": [ { - "date": "2015-08-03T12:00:00.000Z", + "date": 1440849600000, "value": 20 }, { - "date":"2015-08-04T12:00:00.000Z", + "date":1440856800000, "value":22 } ] diff --git a/backend/tests/condition/AverageOnValueTest.ts b/backend/tests/condition/AverageOnValueTest.ts index de52395..67369f9 100644 --- a/backend/tests/condition/AverageOnValueTest.ts +++ b/backend/tests/condition/AverageOnValueTest.ts @@ -78,6 +78,128 @@ describe('Test AverageOnValueTest', () => { }); }); + describe('separate data', () => { + + it('should separate data correctly', () => { + + var values:any[] = [ + { + date: "946771200000", + value: 100 + }, + { + date: "946857600000", + value: 101 + }, + { + date: "946944000000", + value: 99 + }, + { + date: "947030400000", + value: 102 + }, + { + date: "947116800000", + value: 98 + }, + + // OLD/NEW DATA + { + date: "947289600000", + value: 89 + }, + { + date: "947376000000", + value: 90 + }, + { + date: "947462400000", + value: 91 + }, + { + date: "947548800000", + value: 70 + } + ]; + + + var expectedOldValues:number[] = [100, 101, 99, 102, 98]; + var expectedNewValues:number[] = [89, 90, 91, 70]; + + var actualOldValues:number[] = []; + var actualNewValues:number[] = []; + + averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, Clock.getMomentFromString("2000-01-07T00:00:00")); + chai.expect(actualOldValues).to.be.eqls(expectedOldValues); + chai.expect(actualNewValues).to.be.eqls(expectedNewValues); + }); + + + it('should separate data even if these are older than start date', () => { + var values:any[] = [ + { + date: "946771200000", + value: 100 + }, + { + date: "946857600000", + value: 101 + }, + { + date: "946944000000", + value: 99 + }, + { + date: "947030400000", + value: 102 + }, + { + date: "947116800000", + value: 98 + }, + + // OLD/NEW DATA + { + date: "947289600000", + value: 89 + }, + { + date: "947376000000", + value: 90 + }, + { + date: "947462400000", + value: 91 + }, + { + date: "947548800000", + value: 70 + }, + + //too old data, so it won't be in the arrays + { + date: "942796800000", + value: 42 + } + ]; + + + var expectedOldValues:number[] = [100, 101, 99, 102, 98, 42]; + var expectedNewValues:number[] = [89, 90, 91, 70]; + + var actualOldValues:number[] = []; + var actualNewValues:number[] = []; + + averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, conditionDescription.timeBox.dateOfCreation); + chai.expect(actualOldValues).to.be.eqls(expectedOldValues); + chai.expect(actualNewValues).to.be.eqls(expectedNewValues); + }); + + + }); + + /* describe('evaluate method decrease', () => { it('should return true if threshold is reached', () => { @@ -86,23 +208,23 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: '100' }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: '101' }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: '99' }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: '102' }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: '98' } ]; @@ -110,23 +232,23 @@ describe('Test AverageOnValueTest', () => { var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: '89' }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: '90' }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: '91' }, { - date: "2000-01-11T00:00:00", + date: "947548800000", value: '70' }, { - date: "2000-01-12T00:00:00", + date: "947635200000", value: '110' } ]; @@ -135,7 +257,7 @@ describe('Test AverageOnValueTest', () => { data[aSymbolicName] = oldValues.concat(newValues); var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); it('should return true if threshold is reached with different number of measures', () => { @@ -143,38 +265,38 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: '100' }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: '101' }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: '99' }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: '102' }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: '98' } ]; var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: '89' }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: '90' }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: '91' } ]; @@ -183,7 +305,7 @@ describe('Test AverageOnValueTest', () => { data[aSymbolicName] = oldValues.concat(newValues); var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); it('should return false if threshold is close but not reached', () => { @@ -192,23 +314,23 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: '100' }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: '101' }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: '99' }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: '102' }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: '98' } ]; @@ -216,15 +338,15 @@ describe('Test AverageOnValueTest', () => { var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: '89' }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: '91' }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: '91' } ]; @@ -233,7 +355,7 @@ describe('Test AverageOnValueTest', () => { var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.false; + chai.expect(result.achieved).to.be.false; }); }); @@ -252,23 +374,23 @@ describe('Test AverageOnValueTest', () => { // average : 100 oldValues = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: 100 }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: 101 }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: 99 }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: 102 }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: 98 } ]; @@ -278,15 +400,15 @@ describe('Test AverageOnValueTest', () => { // average : 100 newValues = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 100 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 101 }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: 99 } ]; @@ -305,15 +427,15 @@ describe('Test AverageOnValueTest', () => { // average : 95 newValues = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 90 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 100 }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: 95 } ]; @@ -336,15 +458,15 @@ describe('Test AverageOnValueTest', () => { // average : 95 newValues = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 90 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 100 }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: 95 } ]; @@ -363,19 +485,19 @@ describe('Test AverageOnValueTest', () => { //85.95.91.89 newValues = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: '85' }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: '95' }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: '91' }, { - date: "2000-01-11T00:00:00", + date: "947548800000", value: '89' } ]; @@ -390,128 +512,6 @@ describe('Test AverageOnValueTest', () => { }); - describe('separate data', () => { - - it('should separate data correctly', () => { - - var values:any[] = [ - { - date: "2000-01-02T00:00:00", - value: 100 - }, - { - date: "2000-01-03T00:00:00", - value: 101 - }, - { - date: "2000-01-04T00:00:00", - value: 99 - }, - { - date: "2000-01-05T00:00:00", - value: 102 - }, - { - date: "2000-01-06T00:00:00", - value: 98 - }, - - // OLD/NEW DATA - { - date: "2000-01-08T00:00:00", - value: 89 - }, - { - date: "2000-01-09T00:00:00", - value: 90 - }, - { - date: "2000-01-10T00:00:00", - value: 91 - }, - { - date: "2000-01-11T00:00:00", - value: 70 - } - ]; - - - var expectedOldValues:number[] = [100, 101, 99, 102, 98]; - var expectedNewValues:number[] = [89, 90, 91, 70]; - - var actualOldValues:number[] = []; - var actualNewValues:number[] = []; - - averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, Clock.getMomentFromString("2000-01-07T00:00:00")); - chai.expect(actualOldValues).to.be.eqls(expectedOldValues); - chai.expect(actualNewValues).to.be.eqls(expectedNewValues); - }); - - - it('should separate data even if these are older than start date', () => { - var values:any[] = [ - { - date: "2000-01-02T00:00:00", - value: 100 - }, - { - date: "2000-01-03T00:00:00", - value: 101 - }, - { - date: "2000-01-04T00:00:00", - value: 99 - }, - { - date: "2000-01-05T00:00:00", - value: 102 - }, - { - date: "2000-01-06T00:00:00", - value: 98 - }, - - // OLD/NEW DATA - { - date: "2000-01-08T00:00:00", - value: 89 - }, - { - date: "2000-01-09T00:00:00", - value: 90 - }, - { - date: "2000-01-10T00:00:00", - value: 91 - }, - { - date: "2000-01-11T00:00:00", - value: 70 - }, - - //too old data, so it won't be in the arrays - { - date: "1999-11-17T00:00:00", - value: 42 - } - ]; - - - var expectedOldValues:number[] = [100, 101, 99, 102, 98, 42]; - var expectedNewValues:number[] = [89, 90, 91, 70]; - - var actualOldValues:number[] = []; - var actualNewValues:number[] = []; - - averageOnValue.separateOldAndNewData(values, actualOldValues, actualNewValues, conditionDescription.timeBox.dateOfCreation); - chai.expect(actualOldValues).to.be.eqls(expectedOldValues); - chai.expect(actualNewValues).to.be.eqls(expectedNewValues); - }); - - - }); - - describe('evaluate method increase', () => { var expressionDescription:any = { @@ -536,23 +536,23 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: '100' }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: '101' }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: '99' }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: '102' }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: '98' } ]; @@ -560,23 +560,23 @@ describe('Test AverageOnValueTest', () => { var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 121 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 110 }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: 119 }, { - date: "2000-01-11T00:00:00", + date: "947548800000", value: 70 }, { - date: "2000-01-12T00:00:00", + date: "947635200000", value: 130 } ]; @@ -585,7 +585,7 @@ describe('Test AverageOnValueTest', () => { data[aSymbolicName] = oldValues.concat(newValues); var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); it('should return true if threshold is reached with different number of measures', () => { @@ -593,38 +593,38 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: 100 }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: 101 }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: 99 }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: 102 }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: 98 } ]; var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 111 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 110 }, { - date: "2000-01-10T00:00:00", + date: "947462400000", value: 109 } ]; @@ -632,7 +632,7 @@ describe('Test AverageOnValueTest', () => { data[aSymbolicName] = oldValues.concat(newValues); var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.true; + chai.expect(result.achieved).to.be.true; }); it('should return false if threshold is close but not reached', () => { @@ -640,38 +640,38 @@ describe('Test AverageOnValueTest', () => { var oldValues:any[] = [ { - date: "2000-01-02T00:00:00", + date: "946771200000", value: 100 }, { - date: "2000-01-03T00:00:00", + date: "946857600000", value: 101 }, { - date: "2000-01-04T00:00:00", + date: "946944000000", value: 99 }, { - date: "2000-01-05T00:00:00", + date: "947030400000", value: 102 }, { - date: "2000-01-06T00:00:00", + date: "947116800000", value: 98 } ]; var newValues:any[] = [ { - date: "2000-01-08T00:00:00", + date: "947289600000", value: 111 }, { - date: "2000-01-09T00:00:00", + date: "947376000000", value: 109 }, { - date: "2000-01-10T00:00:00" + '', + date: "947462400000" + '', value: 109 } ]; @@ -680,10 +680,11 @@ describe('Test AverageOnValueTest', () => { data[aSymbolicName] = oldValues.concat(newValues); var result:any = averageOnValue.evaluate(data, conditionDescription); - chai.expect(result.finished).to.be.false; + chai.expect(result.achieved).to.be.false; }); }); + */ }); \ No newline at end of file diff --git a/backend/tests/condition/ConditionTest.ts b/backend/tests/condition/ConditionTest.ts index d34f906..46fbf06 100644 --- a/backend/tests/condition/ConditionTest.ts +++ b/backend/tests/condition/ConditionTest.ts @@ -99,10 +99,10 @@ describe('Test Condition', () => { var data:any = {}; data[aSymbolicName] = [ - {date: "2000-09-01T00:00:00", value: 10}, - {date: "2000-09-01T00:00:00", value: 10}, - {date: "2000-09-01T00:00:00", value: 10}, - {date: "2000-09-01T00:00:00", value: 10} + {date: "967766400000", value: 10}, + {date: "967766400000", value: 10}, + {date: "967766400000", value: 10}, + {date: "967766400000", value: 10} ]; @@ -114,10 +114,10 @@ describe('Test Condition', () => { it('should keep everything if everything is in the timeBox', () => { var data:any = {}; data[aSymbolicName] = [ - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10} + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10} ]; var result:any[] = condition.keepUsefulValues(data, conditionDescription); @@ -127,20 +127,20 @@ describe('Test Condition', () => { it('should keep what is in the timeBox', () => { var expected:any = {}; expected[aSymbolicName] = [ - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10} + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10} ]; var data:any = {}; data[aSymbolicName] = [ - {date: "1999-01-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-09-01T00:00:00", value: 10}, + {date: "915148800000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "967766400000", value: 10}, ]; var result:any[] = condition.keepUsefulValues(data, conditionDescription); diff --git a/backend/tests/condition/OverallGoalConditionTest.ts b/backend/tests/condition/OverallGoalConditionTest.ts index 3b0fdfc..ee09f54 100644 --- a/backend/tests/condition/OverallGoalConditionTest.ts +++ b/backend/tests/condition/OverallGoalConditionTest.ts @@ -59,13 +59,13 @@ describe('Test OverallGoalCondition', () => { var data:any = {}; var values:any[] = [ - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 16}, - {date: "2000-02-01T00:00:00", value: 18} + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 16}, + {date: "949363200000", value: 18} ]; data[aSymbolicName] = values; @@ -77,16 +77,16 @@ describe('Test OverallGoalCondition', () => { it('should return false if min threshold is not reached', () => { var data:any = {}; var values:any[] = [ - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10} + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10} ]; data[aSymbolicName] = values; @@ -99,16 +99,16 @@ describe('Test OverallGoalCondition', () => { var data:any = {}; var values:any[] = [ - {date: "2000-02-01T00:00:00", value: 17}, - {date: "2000-02-01T00:00:00", value: 16}, - {date: "2000-02-01T00:00:00", value: 16}, - {date: "2000-02-01T00:00:00", value: 17}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 19}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 17}, - {date: "2000-02-01T00:00:00", value: 10}, - {date: "2000-02-01T00:00:00", value: 10} + {date: "949363200000", value: 17}, + {date: "949363200000", value: 16}, + {date: "949363200000", value: 16}, + {date: "949363200000", value: 17}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 19}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 17}, + {date: "949363200000", value: 10}, + {date: "949363200000", value: 10} ]; data[aSymbolicName] = values; @@ -122,16 +122,16 @@ describe('Test OverallGoalCondition', () => { var data:any = {}; var values:any[] = [ - {date: "2000-02-01T00:00:00", value: 16}, - {date: "2000-02-01T00:00:00", value: 17}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 19}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 17}, - {date: "2000-02-01T00:00:00", value: 18}, - {date: "2000-02-01T00:00:00", value: 16}, - {date: "2000-02-01T00:00:00", value: 17} + {date: "949363200000", value: 16}, + {date: "949363200000", value: 17}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 19}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 17}, + {date: "949363200000", value: 18}, + {date: "949363200000", value: 16}, + {date: "949363200000", value: 17} ]; data[aSymbolicName] = values; diff --git a/backend/tests/integration/ChallengeIntegrationTest.ts b/backend/tests/integration/ChallengeIntegrationTest.ts index a0e5b75..d39c7e8 100644 --- a/backend/tests/integration/ChallengeIntegrationTest.ts +++ b/backend/tests/integration/ChallengeIntegrationTest.ts @@ -46,7 +46,7 @@ describe('UserChallenge integration test', () => { var loginRouter:LoginRouter = new LoginRouter(context); var aUsername:string = 'Charlie'; - var aGoalID:string = '9bddaf87-5065-4df7-920a-d1d249c9171d'; + var aGoalID:string = '3221c575-85ca-447b-86f3-3a4ef39985dc'; var requestForLogin:any = { @@ -74,41 +74,35 @@ describe('UserChallenge integration test', () => { }); it('should not throw', () => { - dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); + dashboardRouter.createUserChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); }); it('should have added given challenge to current user', () => { - var challenge = dashboardRouter.createChallenge(token, aGoalID,Clock.getMomentFromString("2015-08-05T12:15:00")); - var expected = [challenge.getId()]; + var challenge = dashboardRouter.createUserChallenge(token, aGoalID,Clock.getMomentFromString("2015-08-05T12:15:00")); + var expected = [challenge.getID()]; var result = user.getCurrentChallenges(); chai.expect(result).to.be.eqls(expected); }); it('should have initialized the new challenge status to "RUN" when challenge is created during a working week', () => { - var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); + var newChallenge = dashboardRouter.createUserChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-05T12:15:00")); chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.RUN); }); it('should have initialized the new challenge status to "WAITING" when challenge is created during week-end', () => { - var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-08T12:15:00")); + var newChallenge = dashboardRouter.createUserChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-08T12:15:00")); chai.expect(newChallenge.getStatus()).to.be.eq(ChallengeStatus.WAIT); }); it('should have set the startDate to monday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); + var newChallenge = dashboardRouter.createUserChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); chai.expect(newChallenge.getStartDate().toISOString()).to.be.eq(Clock.getMomentFromString("2015-08-03T00:00:00.000").toISOString()); }); it('should have set the endDate to friday if goal is "week recurrent"', () => { - var newChallenge = dashboardRouter.createChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); + var newChallenge = dashboardRouter.createUserChallenge(token, aGoalID, Clock.getMomentFromString("2015-08-07T12:15:00")); chai.expect(newChallenge.getEndDate().toISOString()).to.be.eq(Clock.getMomentFromString("2015-08-07T23:59:59.999").toISOString()); }); }); - describe('Evaluate a challenge', () => { - var user = loginRouter.checkUserProfile(requestForLogin); - var challenge = dashboardRouter.createChallenge(user.getUUID(), aGoalID, Clock.getMomentFromString("2015-08-03T12:15:00")); - var result:any = dashboardRouter.evaluateChallengesForGivenUser(user); - - }); }); \ No newline at end of file diff --git a/frontend/app/index.html b/frontend/app/index.html index 67c2b5c..668994b 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -9,7 +9,6 @@ - @@ -88,13 +87,11 @@ - - diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index e55bf4e..7459571 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -10,13 +10,11 @@ */ var app = angular .module('ecoknowledgeApp', [ - 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', - 'ngTouch', - 'datePicker' + 'ngTouch' ]) .config(function ($routeProvider) { diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js index e075c96..5a9629b 100644 --- a/frontend/app/scripts/controllers/ServiceDashboard.js +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -8,7 +8,7 @@ app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function Ser this.get = function (successFunc, failFunc, dashboardWanted) { - console.log('TOKEN???', $cookies.get('token')); + console.log('Token stored : ', $cookies.get('token')); var path = dashboardBasePath + '/view/' +$cookies.get('token') + '/' + $cookies.get('dashboardWanted'); @@ -16,13 +16,17 @@ app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function Ser $http.get(path) .success(function (data) { - var goals = data.data.goals; + console.log("DATA RECEIVED BY SERVICE DASHBOARD", data); + + var canTake = data.data.goals.canTakeGoal; + var goals = data.data.goals.goalsData; var badges = data.data.badges; var challenges = data.data.challenges; var dashboardViews = data.data.dashboardList; - successFunc(data, goals, badges, challenges, dashboardViews); + + successFunc(data, canTake, goals, badges, challenges, dashboardViews); }) .error(function (data) { console.error('ServiceDashboard : fail get dashboard', data); @@ -30,11 +34,11 @@ app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function Ser }); }; - this.takeGoal = function (goalID, successFunc, failFunc) { + this.takeGoal = function (info, successFunc, failFunc) { var path = dashboardBasePath + 'takeGoal'; console.log('ServiceDashboard : Take goal', path); - $http.post(path, goalID) + $http.post(path, info) .success(function (data) { successFunc(data); }) @@ -44,7 +48,7 @@ app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function Ser }; this.deleteChallenge = function (idGoal, successFunc, failFunc) { - var path = dashboardBasePath + 'delete/' + idGoal; + var path = dashboardBasePath + 'delete/' + $cookies.get('token') + '/'+ idGoal; console.log('ServiceDashboard : Delete challenge ', path); $http.delete(path) diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index 6b29aae..8ca1738 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -5,6 +5,7 @@ var app = angular.module('ecoknowledgeApp'); app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$cookies', function (ServiceDashboard, $window, $location, $cookies) { var self = this; + self.canTakeChallenge = true; self.goals = {}; self.trophies = {}; self.challenges = {}; @@ -15,19 +16,23 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$c // Debug self.request = {}; + self.debug = {}; + this.getDashboard = function () { - console.log('Angular wanna get the dashboard'); + console.log('\n------------------------------------------------------------\nAngular wanna get the dashboard'); ServiceDashboard.get( - function (data, goals, badges, challenges, dashboardViews) { - console.log('Result of dashboard : ', goals, badges, challenges); + function (data, canTake, goals, badges, challenges, dashboardViews) { self.request = data; + self.canTakeChallenge = canTake; self.goals = goals; self.trophies = badges; self.challenges = challenges; self.dashboardViews = dashboardViews; + + self.debug = challenges; }, function (data) { console.error('Redirection vers', data.redirectTo); @@ -35,17 +40,18 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$c }); }; - this.changeDashboardView = function() { + this.changeDashboardView = function () { console.log('Angular wanna change the dashboard'); $cookies.put('dashboardWanted', self.dashboardWanted); ServiceDashboard.get( - function (data, goals, badges, challenges, dashboardViews) { + function (data, cantTake, goals, badges, challenges, dashboardViews) { console.log('Result of dashboard : ', goals, badges, challenges); self.request = data; + self.canTakeChallenge = cantTake; self.goals = goals; self.trophies = badges; self.challenges = challenges; @@ -54,12 +60,17 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$c function (data) { console.error('Redirection vers', data.redirectTo); $location.path(data.redirectTo); - }, self.dashboardWanted ); + }, self.dashboardWanted); }; self.takeGoal = function (goalID) { - var toSend = {}; - toSend.id = goalID; + + var toSend = { + goalID: goalID, + userID: $cookies.get('token'), + target: $cookies.get('dashboardWanted') + }; + ServiceDashboard.takeGoal(toSend, function (data) { diff --git a/frontend/app/scripts/controllers/login.js b/frontend/app/scripts/controllers/login.js index 3c4c4d9..5ba3331 100644 --- a/frontend/app/scripts/controllers/login.js +++ b/frontend/app/scripts/controllers/login.js @@ -11,9 +11,14 @@ app.controller('LoginCtrl', ['ServiceLogin', '$rootScope', '$location', '$cookie ServiceLogin.login(self.username, function (data) { console.log('Login success: data received', data); + $cookies.put('token', data.data.token); + $cookies.put('dashboardWanted', 'personal'); + console.log('Token stored : ', $cookies.get('token')); + var pathToDashboard = '/dashboard/view/' + data.data.token; + console.log('Redirection vers', pathToDashboard); $location.path(pathToDashboard); }, diff --git a/frontend/app/styles/homepage-challenges.css b/frontend/app/styles/homepage-challenges.css index c047893..a5d097b 100644 --- a/frontend/app/styles/homepage-challenges.css +++ b/frontend/app/styles/homepage-challenges.css @@ -1,36 +1,42 @@ -.challenges-list{ +.challenges-list { list-style-type: none; margin: 0; padding: 0; } + .challenge-btn { - float:right; + float: right; } + .challenge-item { list-style-type: none; margin-top: 10px; text-align: center; - font-size:150%; + font-size: 150%; } + .challenge-name { - text-align:center; + text-align: center; } .challenge-container { - width:70%; - background-color:#F5F6F6; - margin-bottom : 35px; - width:550px; - padding:15px; + width: 70%; + margin-bottom: 35px; + width: 550px; + padding: 15px; +} + +.user { + background-color: #5ECD6D; } .challenge-container table { - width:100%; - height:100%; + width: 100%; + height: 100%; } .challenge-container .challenge-name, .challenge-container .challenge-points { - margin-bottom:15px; + margin-bottom: 15px; } .challenge-container .challenge-desc { @@ -49,19 +55,23 @@ } .challenge-status-WAIT { - background-color:grey; + background-color: grey; opacity: 0.5; } .challenge-status-RUNNING { - background-color:#28a4c9; + background-color: #28a4c9; } .challenge-status-SUCCESS { - background-color:green; + background-color: green; } .challenge-status-FAIL { - background-color:red; + background-color: red; +} + +.challenge-date { + font-size:80%; } diff --git a/frontend/app/views/dashboard.html b/frontend/app/views/dashboard.html index 51ac8bc..7aadabc 100644 --- a/frontend/app/views/dashboard.html +++ b/frontend/app/views/dashboard.html @@ -3,18 +3,12 @@
-{{dashboard.dashboardViews}} - - -
diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index 3b53eb6..28ce400 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -1,47 +1,55 @@ -
+
- - - + - + -
-

{{challenge.name}}

-
- Du {{challenge.startDate | date:'dd/MM/yyyy - HH:mm'}} au {{challenge.endDate | date:'dd/MM/yyyy - HH:mm'}} +

Objectif de {{challenge.user}}

+ {{challenge.goal}} - + {{challenge.startDate | date:'dd/MM/yyyy - HH:mm'}} au {{challenge.endDate | date:'dd/MM/yyyy - HH:mm'}} +
+ 'progress-bar-danger':challenge.progress.durationAchieved > 80, + 'progress-bar-warning':challenge.progress.durationAchieved > 30 && challenge.progress.durationAchieved < 80, + 'progress-bar-success':challenge.progress.durationAchieved < 30 + }" role="progressbar" aria-valuenow="{{100-challenge.progress.durationAchieved}}" + aria-valuemin="100" aria-valuemax="0" style="width:{{100-challenge.progress.durationAchieved}}%">
- - - + + + + + + + +
- {{condition.expression.description}} -
+ Conditions +
+
+ - -
-
-
- {{condition.percentageAchieved}}% -
-
+ {{condition.description}}
+
+
+
+ {{condition.percentageAchieved}}% +
+
+
+ diff --git a/frontend/app/views/homepage/homepage-goal.html b/frontend/app/views/homepage/homepage-goal.html index 7801e12..f36bde9 100644 --- a/frontend/app/views/homepage/homepage-goal.html +++ b/frontend/app/views/homepage/homepage-goal.html @@ -1,4 +1,4 @@ diff --git a/frontend/bower.json b/frontend/bower.json index d906190..2f6bd82 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -4,13 +4,11 @@ "dependencies": { "angular": "^1.3.0", "bootstrap": "^3.2.0", - "angular-animate": "^1.3.0", "angular-cookies": "^1.3.0", "angular-resource": "^1.3.0", "angular-route": "^1.3.0", "angular-sanitize": "^1.3.0", - "angular-touch": "^1.3.0", - "angular-datepicker": "https://github.com/g00fy-/angular-datepicker.git#~1.0.12" + "angular-touch": "^1.3.0" }, "devDependencies": { "angular-mocks": "^1.3.0" diff --git a/frontend/test/karma.conf.js b/frontend/test/karma.conf.js index 6dda265..dd1cda0 100644 --- a/frontend/test/karma.conf.js +++ b/frontend/test/karma.conf.js @@ -25,13 +25,11 @@ module.exports = function(config) { 'bower_components/jquery/dist/jquery.js', 'bower_components/angular/angular.js', 'bower_components/bootstrap/dist/js/bootstrap.js', - 'bower_components/angular-animate/angular-animate.js', 'bower_components/angular-cookies/angular-cookies.js', 'bower_components/angular-resource/angular-resource.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-sanitize/angular-sanitize.js', 'bower_components/angular-touch/angular-touch.js', - 'bower_components/angular-datepicker/dist/index.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower "app/scripts/**/*.js", From 1dd0d9196ab32bae7089598aae52671b45c71238 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 09:50:21 +0200 Subject: [PATCH 18/28] Fix display error in challenges Fix 'binary' behavior : in a team challenge description, each members is displayed with its global current progression --- backend/src/goal/Goal.ts | 7 +++++-- frontend/app/views/homepage/homepage-challenge.html | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index 3da1484..3a90151 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -90,6 +90,8 @@ class Goal { var numberOfConditions:number = 0; var numberOfConditionsAchieved:number = 0; + var globalPercentageAchieved:number = 0; + var achieved:boolean = true; var conditionsDescription:any[] =[]; @@ -101,8 +103,9 @@ class Goal { var currentConditionState = currentCondition.evaluate(currentConditionDescription.values, currentConditionDescription); conditionsDescription.push(currentConditionState); - achieved = achieved && currentConditionState.achieved; + globalPercentageAchieved += currentConditionState.percentageAchieved; + result[currentCondition.getID()] = currentConditionState; numberOfConditions++; @@ -111,7 +114,7 @@ class Goal { result['conditions'] = conditionsDescription; - var percentageAchieved:number = (numberOfConditionsAchieved * 100) / numberOfConditions; + var percentageAchieved:number = (globalPercentageAchieved) / numberOfConditions; result['percentageAchieved'] = percentageAchieved; result['achieved'] = achieved; diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index 28ce400..30b9eca 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -34,6 +34,7 @@ @@ -43,7 +44,7 @@
- {{condition.percentageAchieved}}% + {{condition.percentageAchieved | number}}%
@@ -53,5 +54,4 @@
+ {{condition.description}}
-
From 3b7ece0512e60108ea7c1595f57716c4c9e4d7f8 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 10:23:10 +0200 Subject: [PATCH 19/28] TeamChallenges can be removed A team challenge can be removed, and child user challenges are removed too --- backend/src/api/DashboardRouter.ts | 70 ++++++++++++------- backend/src/challenge/TeamChallenge.ts | 15 +++- backend/src/challenge/TeamChallengeFactory.ts | 4 +- backend/src/challenge/UserChallenge.ts | 6 +- backend/src/user/Team.ts | 17 +++-- .../scripts/controllers/ServiceDashboard.js | 4 +- frontend/app/scripts/controllers/dashboard.js | 2 +- .../views/homepage/homepage-challenge.html | 2 +- 8 files changed, 81 insertions(+), 39 deletions(-) diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 67f73c0..0c5ae46 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -160,7 +160,7 @@ class DashboardRouter extends RouterItf { }); - this.router.delete('/delete/:id/:challengeID', function (req, res) { + this.router.delete('/delete/:id/:challengeID/:target', function (req, res) { self.deleteChallenge(req, res); }); @@ -243,9 +243,29 @@ class DashboardRouter extends RouterItf { var challengeID = req.params.challengeID; var userID = req.params.id; - var user:User = null; + var target = req.params.target; + + console.log("TARGET", target); + try { - user = this.checkUserID(userID); + if (target == 'personal') { + + var user:User = null; + user = this.checkUserID(userID); + + user.deleteChallenge(challengeID); + res.send({"success": "Objectif supprimé !"}); + } + else { + + var team:Team = null; + team = this.checkTeamID(target); + + var challengeToDelete = this.teamChallengeRepository.getChallengeByID(challengeID); + + team.deleteChallenge(challengeToDelete); + res.send({"success": "Objectif supprimé !"}); + } } catch (e) { if (e instanceof BadRequestException) { @@ -263,14 +283,6 @@ class DashboardRouter extends RouterItf { return; } } - - try { - user.deleteChallenge(challengeID); - res.send({"success": "Objectif supprimé !"}); - } - catch (e) { - res.send({error: e.toString()}); - } } getTeamDashboardForAMember(teamDescriptionWanted:Team):any { @@ -494,12 +506,12 @@ class DashboardRouter extends RouterItf { this.addFinishedBadge(challengeID, entity.getUUID()); /* - // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); - if (newChallenge != null) { - self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); - } - */ + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); + } + */ } // Check if the challenge is not achieved but finished @@ -509,12 +521,12 @@ class DashboardRouter extends RouterItf { entity.deleteChallenge(challengeToEvaluate.getID()); /* - // Build the new challenge (recurring) and evaluate it - var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); - if (newChallenge != null) { - self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); - } - */ + // Build the new challenge (recurring) and evaluate it + var newChallenge = self.createTeamChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); + if (newChallenge != null) { + self.evaluateTeamChallenge(entity, newChallenge, newChallenge.getID()); + } + */ } return challengeToEvaluate; @@ -639,8 +651,7 @@ class DashboardRouter extends RouterItf { var goal:Goal = this.goalRepository.getGoal(goalID); - - var newChallenge = team.addChallenge(goal,this.userChallengeRepository, date); + var newChallenge = team.addChallenge(goal, this.userChallengeRepository, date); if (newChallenge == null) { return null; @@ -651,6 +662,15 @@ class DashboardRouter extends RouterItf { } + checkTeamID(teamID):Team { + var currentTeam:Team = this.teamRepository.getTeam(teamID); + if (currentTeam == null) { + throw new BadArgumentException('Given team can not be found'); + } + + return currentTeam; + } + checkUserID(userID):User { var currentUser:User = this.userRepository.getUser(userID); diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index 5f83e33..9187dc4 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -9,6 +9,7 @@ var moment_timezone = require('moment-timezone'); import Status= require('../Status'); import UserChallenge= require('./UserChallenge'); +import UserChallengeRepository= require('./UserChallengeRepository'); import Team= require('../user/Team'); import BadArgumentException = require('../exceptions/BadArgumentException'); @@ -18,6 +19,7 @@ class TeamChallenge { private team:Team; private childChallenges:UserChallenge[]; + private userChallengeRepository:UserChallengeRepository; private status:Status; @@ -27,15 +29,17 @@ class TeamChallenge { private durationAchieved:number; private progress:any = {}; - constructor(team:Team, childChallenges:UserChallenge[], id = null) { + constructor(id, team:Team, childChallenges:UserChallenge[], userChallengeRepository:UserChallengeRepository) { if (childChallenges.length == 0) { throw new BadArgumentException('Can not build team challenge because there is no child challenges to attach'); } - this.id = (id == null) ? uuid.v4() : id; + this.id = id; this.team = team; this.childChallenges = childChallenges; + this.userChallengeRepository = userChallengeRepository; + this.status = this.getStatus(); this.checkChildrenTimeBoxes(); } @@ -76,6 +80,13 @@ class TeamChallenge { return this.childChallenges[0].haveToStart(now); } + removeFromMembers() { + for(var currentUserChallengeIndex in this.childChallenges) { + var currentUserChallenge:UserChallenge = this.childChallenges[currentUserChallengeIndex]; + currentUserChallenge.removeFromInternalUser(); + } + } + getStatus():Status { var succeed:boolean = false; var stillRunning:boolean = false; diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts index d5d7512..22abe85 100644 --- a/backend/src/challenge/TeamChallengeFactory.ts +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -4,6 +4,7 @@ import Goal = require('../goal/Goal'); import UserChallenge = require('../challenge/UserChallenge'); import TeamChallenge = require('../challenge/TeamChallenge'); import UserChallengeRepository = require('../challenge/UserChallengeRepository'); +import uuid = require('node-uuid'); class TeamChallengeFactory { createTeamChallenge(team:Team, goal:Goal, userChallengeRepository:UserChallengeRepository, now) { @@ -21,8 +22,9 @@ class TeamChallengeFactory { membersChallenges.push(currentUserChallenge); console.log("Le user", currentMember.getName(), "has this challenge added", currentUserChallenge.getID()); } + var id = uuid.v4(); - return new TeamChallenge(team, membersChallenges); + return new TeamChallenge(id, team, membersChallenges, userChallengeRepository); } restoreTeamChallenge() { diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 925c103..b5fd42b 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -117,6 +117,10 @@ class UserChallenge { this.status = badgeStatus; } + removeFromInternalUser() { + this.user.deleteChallenge(this.getID()); + } + getSensors():any { var required:any = {}; @@ -307,7 +311,7 @@ class UserChallenge { getDataForClient():any { return { id: this.id, - type:'user', + type:'personal', startDate: this.startDate, endDate: this.endDate, goal: this.goal.getName(), diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 6014467..9de24fe 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -79,17 +79,22 @@ class Team { return newChallenge; } - deleteChallenge(challengeID:string):void { + deleteChallenge(challenge:TeamChallenge):void { + + var challengeIndex:number = this.getChallengeByID(challenge.getID()); - var challengeIndex:number = this.getChallengeByID(challengeID); if (challengeIndex == -1) { throw new BadArgumentException('Can not find given challenge ID'); } - else { - this.currentChallenges.splice(challengeIndex, 1); - } - console.log("UserChallenge deleted ! Current challenges:", this.currentChallenges); + // Remove team challenge from team + this.currentChallenges.splice(challengeIndex, 1); + + // Remove challenges from team's members + challenge.removeFromMembers(); + + + console.log("TeamChallenge deleted ! Current challenges:", this.currentChallenges); } private getChallengeByID(challengeID:string):number { diff --git a/frontend/app/scripts/controllers/ServiceDashboard.js b/frontend/app/scripts/controllers/ServiceDashboard.js index 5a9629b..7cacf0f 100644 --- a/frontend/app/scripts/controllers/ServiceDashboard.js +++ b/frontend/app/scripts/controllers/ServiceDashboard.js @@ -47,8 +47,8 @@ app.service('ServiceDashboard', ['$http', '$rootScope', '$cookies', function Ser }); }; - this.deleteChallenge = function (idGoal, successFunc, failFunc) { - var path = dashboardBasePath + 'delete/' + $cookies.get('token') + '/'+ idGoal; + this.deleteChallenge = function (idGoal, target, successFunc, failFunc) { + var path = dashboardBasePath + 'delete/' + $cookies.get('token') + '/'+ idGoal + '/' + $cookies.get('dashboardWanted'); console.log('ServiceDashboard : Delete challenge ', path); $http.delete(path) diff --git a/frontend/app/scripts/controllers/dashboard.js b/frontend/app/scripts/controllers/dashboard.js index 8ca1738..e74c795 100644 --- a/frontend/app/scripts/controllers/dashboard.js +++ b/frontend/app/scripts/controllers/dashboard.js @@ -84,7 +84,7 @@ app.controller('DashboardCtrl', ['ServiceDashboard', '$window', '$location', '$c self.deleteChallenge = function (objective) { - ServiceDashboard.deleteChallenge(objective.id, + ServiceDashboard.deleteChallenge(objective.id,objective.type, function (data) { console.log('DashboardCtrl : Delete challenge : data RECEIVED :', data); diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index 30b9eca..a61f9d7 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -26,7 +26,7 @@ - Conditions + Détails de la progression
From 7875b0c2f666865a2117b5c92d29859628b075d5 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 10:33:35 +0200 Subject: [PATCH 20/28] Add color to separate personnal and team challenges Add a switch to distinguish user's challenges (personal) to team's challenges --- backend/src/challenge/TeamChallengeFactory.ts | 6 +++++- backend/src/challenge/UserChallenge.ts | 10 ++++++++-- backend/src/challenge/UserChallengeFactory.ts | 8 ++++---- backend/src/user/User.ts | 4 ++-- frontend/app/styles/homepage-challenges.css | 4 ++++ frontend/app/views/homepage/homepage-challenge.html | 4 ++-- 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts index 22abe85..a258cbd 100644 --- a/backend/src/challenge/TeamChallengeFactory.ts +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -17,9 +17,13 @@ class TeamChallengeFactory { for(var currentMemberIndex in members) { var currentMember:User = members[currentMemberIndex]; - var currentUserChallenge:UserChallenge = currentMember.addChallenge(goal, now); + + var currentUserChallenge:UserChallenge = currentMember.addChallenge(goal, now, team.getName()); + userChallengeRepository.addUserChallenge(currentUserChallenge); + membersChallenges.push(currentUserChallenge); + console.log("Le user", currentMember.getName(), "has this challenge added", currentUserChallenge.getID()); } var id = uuid.v4(); diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index b5fd42b..e039d03 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -31,8 +31,9 @@ class UserChallenge { private mapSymbolicNameToSensor:any = {}; private user:User; + private takenBy:string; - constructor(id:string, goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, mapConditionIDToSensorAndTimeBoxRequired:any) { + constructor(id:string, goal:Goal, user:User, startDate:moment.Moment, endDate:moment.Moment, mapConditionIDToSensorAndTimeBoxRequired:any, takenBy = null) { this.id = id; @@ -42,6 +43,7 @@ class UserChallenge { this.goal = goal; this.user = user; + this.takenBy = (takenBy == null) ? this.user.getName() : takenBy; this.mapConditionIDToSensorAndTimeBoxRequired = mapConditionIDToSensorAndTimeBoxRequired; this.mapSymbolicNameToSensor = this.user.getMapSymbolicNameToSensor(); @@ -309,13 +311,17 @@ class UserChallenge { } getDataForClient():any { + + var type = (this.takenBy == this.user.getName()) ? 'personal' : 'team'; + return { id: this.id, - type:'personal', + type:type, startDate: this.startDate, endDate: this.endDate, goal: this.goal.getName(), user: this.user.getName(), + takenBy:this.takenBy, progress: this.progress, status:this.status } diff --git a/backend/src/challenge/UserChallengeFactory.ts b/backend/src/challenge/UserChallengeFactory.ts index ae8cdb4..8e7a723 100644 --- a/backend/src/challenge/UserChallengeFactory.ts +++ b/backend/src/challenge/UserChallengeFactory.ts @@ -46,7 +46,7 @@ class GoalInstanceFactory { return challenge; } - createChallenge(goal:Goal, user:User, now:moment.Moment):UserChallenge { + createChallenge(goal:Goal, user:User, now:moment.Moment, takenBy = null):UserChallenge { this.checkDataFromCreate(goal, user, now); var challengeID = UUID.v4(); @@ -68,13 +68,13 @@ class GoalInstanceFactory { } - var result = this.newChallenge(challengeID, goal, user, mapConditionIDToSensorAndTimeBoxRequired, startDateOfChallenge, endDateOfChallenge, now); + var result = this.newChallenge(challengeID, goal, user, mapConditionIDToSensorAndTimeBoxRequired, startDateOfChallenge, endDateOfChallenge, now, takenBy); return result; } - private newChallenge(challengeID, goal, user, mapConditionIDToSymbolicNamesAndTimeBoxesRequired, startDate, endDate, now):UserChallenge { - var newChallenge:UserChallenge = new UserChallenge(challengeID, goal, user, startDate, endDate, mapConditionIDToSymbolicNamesAndTimeBoxesRequired); + private newChallenge(challengeID, goal, user, mapConditionIDToSymbolicNamesAndTimeBoxesRequired, startDate, endDate, now, takenBy = null):UserChallenge { + var newChallenge:UserChallenge = new UserChallenge(challengeID, goal, user, startDate, endDate, mapConditionIDToSymbolicNamesAndTimeBoxesRequired, takenBy); if (newChallenge.getEndDate().isAfter(goal.getEndOfValidityPeriod())) { return null; diff --git a/backend/src/user/User.ts b/backend/src/user/User.ts index af8287e..58f7109 100644 --- a/backend/src/user/User.ts +++ b/backend/src/user/User.ts @@ -83,8 +83,8 @@ class User { this.currentChallenges = []; } - addChallenge(goal:Goal, now:moment.Moment):Challenge { - var newChallenge = this.challengeFactory.createChallenge(goal, this, now); + addChallenge(goal:Goal, now:moment.Moment, takenBy = null):Challenge { + var newChallenge = this.challengeFactory.createChallenge(goal, this, now, takenBy); // Check if we try if (newChallenge.getEndDate().isAfter(goal.getEndOfValidityPeriod())) { diff --git a/frontend/app/styles/homepage-challenges.css b/frontend/app/styles/homepage-challenges.css index a5d097b..7c89c37 100644 --- a/frontend/app/styles/homepage-challenges.css +++ b/frontend/app/styles/homepage-challenges.css @@ -30,6 +30,10 @@ background-color: #5ECD6D; } +.team { + background-color: #0086b3; +} + .challenge-container table { width: 100%; height: 100%; diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index a61f9d7..9fa7ca6 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -1,9 +1,9 @@ -
+
-

Objectif de {{challenge.user}}

+

Objectif de {{challenge.takenBy}}

{{challenge.goal}} - {{challenge.startDate | date:'dd/MM/yyyy - HH:mm'}} au {{challenge.endDate | date:'dd/MM/yyyy - HH:mm'}} From 2ddbaf7a3ff84ed457126372c8c1d2b5bf0bde87 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 11:17:32 +0200 Subject: [PATCH 21/28] Re-link create goal page Add datepicker bundled directives --- frontend/app/index.html | 3 + frontend/app/scripts/app.js | 8 +- frontend/app/styles/create-goal.css | 13 +++- frontend/app/styles/main.css | 2 +- frontend/app/ui-bootstrap-tpls-0.9.0.min.js | 2 + .../views/create goal/date-validity-goal.html | 5 +- frontend/app/views/create-goal.html | 74 +++++++++++-------- frontend/bower.json | 3 +- frontend/test/karma.conf.js | 1 + 9 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 frontend/app/ui-bootstrap-tpls-0.9.0.min.js diff --git a/frontend/app/index.html b/frontend/app/index.html index 668994b..ef5fc6b 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -9,6 +9,7 @@ + @@ -91,12 +92,14 @@ + + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index 7459571..db6fe31 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -14,7 +14,9 @@ var app = angular 'ngResource', 'ngRoute', 'ngSanitize', - 'ngTouch' + 'ngAnimate', + 'ngTouch', + 'ui.bootstrap' ]) .config(function ($routeProvider) { @@ -39,6 +41,10 @@ var app = angular controller: 'DashboardCtrl', controllerAs:'dashboard' }) + .when('/create-goal', { + templateUrl: '../views/create-goal.html', + controller: 'GoalCtrl' + }) .otherwise({ redirectTo: '/lolerreurdansredirectionangulareuuuh/' }); diff --git a/frontend/app/styles/create-goal.css b/frontend/app/styles/create-goal.css index cffec77..269631e 100644 --- a/frontend/app/styles/create-goal.css +++ b/frontend/app/styles/create-goal.css @@ -2,4 +2,15 @@ color:#fff; background-color: #337ab7; border-color: #2e6da4; -} \ No newline at end of file +} + +.create-goal-container { + width:800px; + margin-left:auto; + margin-right:auto; + padding:50px; +} + +#create-goal-date { + margin-top : 50px; +} diff --git a/frontend/app/styles/main.css b/frontend/app/styles/main.css index fd2a465..ef4cf22 100644 --- a/frontend/app/styles/main.css +++ b/frontend/app/styles/main.css @@ -74,7 +74,7 @@ body { /* Responsive: Portrait tablets and up */ @media screen and (min-width: 768px) { .container { - max-width: 730px; + max-width: 1200px; } /* Remove the padding we set earlier */ diff --git a/frontend/app/ui-bootstrap-tpls-0.9.0.min.js b/frontend/app/ui-bootstrap-tpls-0.9.0.min.js new file mode 100644 index 0000000..da950fb --- /dev/null +++ b/frontend/app/ui-bootstrap-tpls-0.9.0.min.js @@ -0,0 +1,2 @@ +angular.module("ui.bootstrap",["ui.bootstrap.tpls","ui.bootstrap.transition","ui.bootstrap.collapse","ui.bootstrap.accordion","ui.bootstrap.alert","ui.bootstrap.bindHtml","ui.bootstrap.buttons","ui.bootstrap.carousel","ui.bootstrap.position","ui.bootstrap.datepicker","ui.bootstrap.dropdownToggle","ui.bootstrap.modal","ui.bootstrap.pagination","ui.bootstrap.tooltip","ui.bootstrap.popover","ui.bootstrap.progressbar","ui.bootstrap.rating","ui.bootstrap.tabs","ui.bootstrap.timepicker","ui.bootstrap.typeahead"]),angular.module("ui.bootstrap.tpls",["template/accordion/accordion-group.html","template/accordion/accordion.html","template/alert/alert.html","template/carousel/carousel.html","template/carousel/slide.html","template/datepicker/datepicker.html","template/datepicker/popup.html","template/modal/backdrop.html","template/modal/window.html","template/pagination/pager.html","template/pagination/pagination.html","template/tooltip/tooltip-html-unsafe-popup.html","template/tooltip/tooltip-popup.html","template/popover/popover.html","template/progressbar/bar.html","template/progressbar/progress.html","template/progressbar/progressbar.html","template/rating/rating.html","template/tabs/tab.html","template/tabs/tabset.html","template/timepicker/timepicker.html","template/typeahead/typeahead-match.html","template/typeahead/typeahead-popup.html"]),angular.module("ui.bootstrap.transition",[]).factory("$transition",["$q","$timeout","$rootScope",function(a,b,c){function d(a){for(var b in a)if(void 0!==f.style[b])return a[b]}var e=function(d,f,g){g=g||{};var h=a.defer(),i=e[g.animation?"animationEndEventName":"transitionEndEventName"],j=function(){c.$apply(function(){d.unbind(i,j),h.resolve(d)})};return i&&d.bind(i,j),b(function(){angular.isString(f)?d.addClass(f):angular.isFunction(f)?f(d):angular.isObject(f)&&d.css(f),i||h.resolve(d)}),h.promise.cancel=function(){i&&d.unbind(i,j),h.reject("Transition cancelled")},h.promise},f=document.createElement("trans"),g={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",transition:"transitionend"},h={WebkitTransition:"webkitAnimationEnd",MozTransition:"animationend",OTransition:"oAnimationEnd",transition:"animationend"};return e.transitionEndEventName=d(g),e.animationEndEventName=d(h),e}]),angular.module("ui.bootstrap.collapse",["ui.bootstrap.transition"]).directive("collapse",["$transition",function(a){return{link:function(b,c,d){function e(b){function d(){j===e&&(j=void 0)}var e=a(c,b);return j&&j.cancel(),j=e,e.then(d,d),e}function f(){k?(k=!1,g()):(c.removeClass("collapse").addClass("collapsing"),e({height:c[0].scrollHeight+"px"}).then(g))}function g(){c.removeClass("collapsing"),c.addClass("collapse in"),c.css({height:"auto"})}function h(){if(k)k=!1,i(),c.css({height:0});else{c.css({height:c[0].scrollHeight+"px"});{c[0].offsetWidth}c.removeClass("collapse in").addClass("collapsing"),e({height:0}).then(i)}}function i(){c.removeClass("collapsing"),c.addClass("collapse")}var j,k=!0;b.$watch(d.collapse,function(a){a?h():f()})}}}]),angular.module("ui.bootstrap.accordion",["ui.bootstrap.collapse"]).constant("accordionConfig",{closeOthers:!0}).controller("AccordionController",["$scope","$attrs","accordionConfig",function(a,b,c){this.groups=[],this.closeOthers=function(d){var e=angular.isDefined(b.closeOthers)?a.$eval(b.closeOthers):c.closeOthers;e&&angular.forEach(this.groups,function(a){a!==d&&(a.isOpen=!1)})},this.addGroup=function(a){var b=this;this.groups.push(a),a.$on("$destroy",function(){b.removeGroup(a)})},this.removeGroup=function(a){var b=this.groups.indexOf(a);-1!==b&&this.groups.splice(this.groups.indexOf(a),1)}}]).directive("accordion",function(){return{restrict:"EA",controller:"AccordionController",transclude:!0,replace:!1,templateUrl:"template/accordion/accordion.html"}}).directive("accordionGroup",["$parse",function(a){return{require:"^accordion",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/accordion/accordion-group.html",scope:{heading:"@"},controller:function(){this.setHeading=function(a){this.heading=a}},link:function(b,c,d,e){var f,g;e.addGroup(b),b.isOpen=!1,d.isOpen&&(f=a(d.isOpen),g=f.assign,b.$parent.$watch(f,function(a){b.isOpen=!!a})),b.$watch("isOpen",function(a){a&&e.closeOthers(b),g&&g(b.$parent,a)})}}}]).directive("accordionHeading",function(){return{restrict:"EA",transclude:!0,template:"",replace:!0,require:"^accordionGroup",compile:function(a,b,c){return function(a,b,d,e){e.setHeading(c(a,function(){}))}}}}).directive("accordionTransclude",function(){return{require:"^accordionGroup",link:function(a,b,c,d){a.$watch(function(){return d[c.accordionTransclude]},function(a){a&&(b.html(""),b.append(a))})}}}),angular.module("ui.bootstrap.alert",[]).controller("AlertController",["$scope","$attrs",function(a,b){a.closeable="close"in b}]).directive("alert",function(){return{restrict:"EA",controller:"AlertController",templateUrl:"template/alert/alert.html",transclude:!0,replace:!0,scope:{type:"=",close:"&"}}}),angular.module("ui.bootstrap.bindHtml",[]).directive("bindHtmlUnsafe",function(){return function(a,b,c){b.addClass("ng-binding").data("$binding",c.bindHtmlUnsafe),a.$watch(c.bindHtmlUnsafe,function(a){b.html(a||"")})}}),angular.module("ui.bootstrap.buttons",[]).constant("buttonConfig",{activeClass:"active",toggleEvent:"click"}).controller("ButtonsController",["buttonConfig",function(a){this.activeClass=a.activeClass||"active",this.toggleEvent=a.toggleEvent||"click"}]).directive("btnRadio",function(){return{require:["btnRadio","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){var e=d[0],f=d[1];f.$render=function(){b.toggleClass(e.activeClass,angular.equals(f.$modelValue,a.$eval(c.btnRadio)))},b.bind(e.toggleEvent,function(){b.hasClass(e.activeClass)||a.$apply(function(){f.$setViewValue(a.$eval(c.btnRadio)),f.$render()})})}}}).directive("btnCheckbox",function(){return{require:["btnCheckbox","ngModel"],controller:"ButtonsController",link:function(a,b,c,d){function e(){return g(c.btnCheckboxTrue,!0)}function f(){return g(c.btnCheckboxFalse,!1)}function g(b,c){var d=a.$eval(b);return angular.isDefined(d)?d:c}var h=d[0],i=d[1];i.$render=function(){b.toggleClass(h.activeClass,angular.equals(i.$modelValue,e()))},b.bind(h.toggleEvent,function(){a.$apply(function(){i.$setViewValue(b.hasClass(h.activeClass)?f():e()),i.$render()})})}}}),angular.module("ui.bootstrap.carousel",["ui.bootstrap.transition"]).controller("CarouselController",["$scope","$timeout","$transition","$q",function(a,b,c){function d(){e();var c=+a.interval;!isNaN(c)&&c>=0&&(g=b(f,c))}function e(){g&&(b.cancel(g),g=null)}function f(){h?(a.next(),d()):a.pause()}var g,h,i=this,j=i.slides=[],k=-1;i.currentSlide=null;var l=!1;i.select=function(e,f){function g(){if(!l){if(i.currentSlide&&angular.isString(f)&&!a.noTransition&&e.$element){e.$element.addClass(f);{e.$element[0].offsetWidth}angular.forEach(j,function(a){angular.extend(a,{direction:"",entering:!1,leaving:!1,active:!1})}),angular.extend(e,{direction:f,active:!0,entering:!0}),angular.extend(i.currentSlide||{},{direction:f,leaving:!0}),a.$currentTransition=c(e.$element,{}),function(b,c){a.$currentTransition.then(function(){h(b,c)},function(){h(b,c)})}(e,i.currentSlide)}else h(e,i.currentSlide);i.currentSlide=e,k=m,d()}}function h(b,c){angular.extend(b,{direction:"",active:!0,leaving:!1,entering:!1}),angular.extend(c||{},{direction:"",active:!1,leaving:!1,entering:!1}),a.$currentTransition=null}var m=j.indexOf(e);void 0===f&&(f=m>k?"next":"prev"),e&&e!==i.currentSlide&&(a.$currentTransition?(a.$currentTransition.cancel(),b(g)):g())},a.$on("$destroy",function(){l=!0}),i.indexOfSlide=function(a){return j.indexOf(a)},a.next=function(){var b=(k+1)%j.length;return a.$currentTransition?void 0:i.select(j[b],"next")},a.prev=function(){var b=0>k-1?j.length-1:k-1;return a.$currentTransition?void 0:i.select(j[b],"prev")},a.select=function(a){i.select(a)},a.isActive=function(a){return i.currentSlide===a},a.slides=function(){return j},a.$watch("interval",d),a.$on("$destroy",e),a.play=function(){h||(h=!0,d())},a.pause=function(){a.noPause||(h=!1,e())},i.addSlide=function(b,c){b.$element=c,j.push(b),1===j.length||b.active?(i.select(j[j.length-1]),1==j.length&&a.play()):b.active=!1},i.removeSlide=function(a){var b=j.indexOf(a);j.splice(b,1),j.length>0&&a.active?b>=j.length?i.select(j[b-1]):i.select(j[b]):k>b&&k--}}]).directive("carousel",[function(){return{restrict:"EA",transclude:!0,replace:!0,controller:"CarouselController",require:"carousel",templateUrl:"template/carousel/carousel.html",scope:{interval:"=",noTransition:"=",noPause:"="}}}]).directive("slide",["$parse",function(a){return{require:"^carousel",restrict:"EA",transclude:!0,replace:!0,templateUrl:"template/carousel/slide.html",scope:{},link:function(b,c,d,e){if(d.active){var f=a(d.active),g=f.assign,h=b.active=f(b.$parent);b.$watch(function(){var a=f(b.$parent);return a!==b.active&&(a!==h?h=b.active=a:g(b.$parent,a=h=b.active)),a})}e.addSlide(b,c),b.$on("$destroy",function(){e.removeSlide(b)}),b.$watch("active",function(a){a&&e.select(b)})}}}]),angular.module("ui.bootstrap.position",[]).factory("$position",["$document","$window",function(a,b){function c(a,c){return a.currentStyle?a.currentStyle[c]:b.getComputedStyle?b.getComputedStyle(a)[c]:a.style[c]}function d(a){return"static"===(c(a,"position")||"static")}var e=function(b){for(var c=a[0],e=b.offsetParent||c;e&&e!==c&&d(e);)e=e.offsetParent;return e||c};return{position:function(b){var c=this.offset(b),d={top:0,left:0},f=e(b[0]);f!=a[0]&&(d=this.offset(angular.element(f)),d.top+=f.clientTop-f.scrollTop,d.left+=f.clientLeft-f.scrollLeft);var g=b[0].getBoundingClientRect();return{width:g.width||b.prop("offsetWidth"),height:g.height||b.prop("offsetHeight"),top:c.top-d.top,left:c.left-d.left}},offset:function(c){var d=c[0].getBoundingClientRect();return{width:d.width||c.prop("offsetWidth"),height:d.height||c.prop("offsetHeight"),top:d.top+(b.pageYOffset||a[0].body.scrollTop||a[0].documentElement.scrollTop),left:d.left+(b.pageXOffset||a[0].body.scrollLeft||a[0].documentElement.scrollLeft)}}}}]),angular.module("ui.bootstrap.datepicker",["ui.bootstrap.position"]).constant("datepickerConfig",{dayFormat:"dd",monthFormat:"MMMM",yearFormat:"yyyy",dayHeaderFormat:"EEE",dayTitleFormat:"MMMM yyyy",monthTitleFormat:"yyyy",showWeeks:!0,startingDay:0,yearRange:20,minDate:null,maxDate:null}).controller("DatepickerController",["$scope","$attrs","dateFilter","datepickerConfig",function(a,b,c,d){function e(b,c){return angular.isDefined(b)?a.$parent.$eval(b):c}function f(a,b){return new Date(a,b,0).getDate()}function g(a,b){for(var c=new Array(b),d=a,e=0;b>e;)c[e++]=new Date(d),d.setDate(d.getDate()+1);return c}function h(a,b,d,e){return{date:a,label:c(a,b),selected:!!d,secondary:!!e}}var i={day:e(b.dayFormat,d.dayFormat),month:e(b.monthFormat,d.monthFormat),year:e(b.yearFormat,d.yearFormat),dayHeader:e(b.dayHeaderFormat,d.dayHeaderFormat),dayTitle:e(b.dayTitleFormat,d.dayTitleFormat),monthTitle:e(b.monthTitleFormat,d.monthTitleFormat)},j=e(b.startingDay,d.startingDay),k=e(b.yearRange,d.yearRange);this.minDate=d.minDate?new Date(d.minDate):null,this.maxDate=d.maxDate?new Date(d.maxDate):null,this.modes=[{name:"day",getVisibleDates:function(a,b){var d=a.getFullYear(),e=a.getMonth(),k=new Date(d,e,1),l=j-k.getDay(),m=l>0?7-l:-l,n=new Date(k),o=0;m>0&&(n.setDate(-m+1),o+=m),o+=f(d,e+1),o+=(7-o%7)%7;for(var p=g(n,o),q=new Array(7),r=0;o>r;r++){var s=new Date(p[r]);p[r]=h(s,i.day,b&&b.getDate()===s.getDate()&&b.getMonth()===s.getMonth()&&b.getFullYear()===s.getFullYear(),s.getMonth()!==e)}for(var t=0;7>t;t++)q[t]=c(p[t].date,i.dayHeader);return{objects:p,title:c(a,i.dayTitle),labels:q}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth(),a.getDate())-new Date(b.getFullYear(),b.getMonth(),b.getDate())},split:7,step:{months:1}},{name:"month",getVisibleDates:function(a,b){for(var d=new Array(12),e=a.getFullYear(),f=0;12>f;f++){var g=new Date(e,f,1);d[f]=h(g,i.month,b&&b.getMonth()===f&&b.getFullYear()===e)}return{objects:d,title:c(a,i.monthTitle)}},compare:function(a,b){return new Date(a.getFullYear(),a.getMonth())-new Date(b.getFullYear(),b.getMonth())},split:3,step:{years:1}},{name:"year",getVisibleDates:function(a,b){for(var c=new Array(k),d=a.getFullYear(),e=parseInt((d-1)/k,10)*k+1,f=0;k>f;f++){var g=new Date(e+f,0,1);c[f]=h(g,i.year,b&&b.getFullYear()===g.getFullYear())}return{objects:c,title:[c[0].label,c[k-1].label].join(" - ")}},compare:function(a,b){return a.getFullYear()-b.getFullYear()},split:5,step:{years:k}}],this.isDisabled=function(b,c){var d=this.modes[c||0];return this.minDate&&d.compare(b,this.minDate)<0||this.maxDate&&d.compare(b,this.maxDate)>0||a.dateDisabled&&a.dateDisabled({date:b,mode:d.name})}}]).directive("datepicker",["dateFilter","$parse","datepickerConfig","$log",function(a,b,c,d){return{restrict:"EA",replace:!0,templateUrl:"template/datepicker/datepicker.html",scope:{dateDisabled:"&"},require:["datepicker","?^ngModel"],controller:"DatepickerController",link:function(a,e,f,g){function h(){a.showWeekNumbers=0===o&&q}function i(a,b){for(var c=[];a.length>0;)c.push(a.splice(0,b));return c}function j(b){var c=null,e=!0;n.$modelValue&&(c=new Date(n.$modelValue),isNaN(c)?(e=!1,d.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):b&&(p=c)),n.$setValidity("date",e);var f=m.modes[o],g=f.getVisibleDates(p,c);angular.forEach(g.objects,function(a){a.disabled=m.isDisabled(a.date,o)}),n.$setValidity("date-disabled",!c||!m.isDisabled(c)),a.rows=i(g.objects,f.split),a.labels=g.labels||[],a.title=g.title}function k(a){o=a,h(),j()}function l(a){var b=new Date(a);b.setDate(b.getDate()+4-(b.getDay()||7));var c=b.getTime();return b.setMonth(0),b.setDate(1),Math.floor(Math.round((c-b)/864e5)/7)+1}var m=g[0],n=g[1];if(n){var o=0,p=new Date,q=c.showWeeks;f.showWeeks?a.$parent.$watch(b(f.showWeeks),function(a){q=!!a,h()}):h(),f.min&&a.$parent.$watch(b(f.min),function(a){m.minDate=a?new Date(a):null,j()}),f.max&&a.$parent.$watch(b(f.max),function(a){m.maxDate=a?new Date(a):null,j()}),n.$render=function(){j(!0)},a.select=function(a){if(0===o){var b=n.$modelValue?new Date(n.$modelValue):new Date(0,0,0,0,0,0,0);b.setFullYear(a.getFullYear(),a.getMonth(),a.getDate()),n.$setViewValue(b),j(!0)}else p=a,k(o-1)},a.move=function(a){var b=m.modes[o].step;p.setMonth(p.getMonth()+a*(b.months||0)),p.setFullYear(p.getFullYear()+a*(b.years||0)),j()},a.toggleMode=function(){k((o+1)%m.modes.length)},a.getWeekNumber=function(b){return 0===o&&a.showWeekNumbers&&7===b.length?l(b[0].date):null}}}}}]).constant("datepickerPopupConfig",{dateFormat:"yyyy-MM-dd",currentText:"Today",toggleWeeksText:"Weeks",clearText:"Clear",closeText:"Done",closeOnDateSelection:!0,appendToBody:!1,showButtonBar:!0}).directive("datepickerPopup",["$compile","$parse","$document","$position","dateFilter","datepickerPopupConfig","datepickerConfig",function(a,b,c,d,e,f,g){return{restrict:"EA",require:"ngModel",link:function(h,i,j,k){function l(a){u?u(h,!!a):q.isOpen=!!a}function m(a){if(a){if(angular.isDate(a))return k.$setValidity("date",!0),a;if(angular.isString(a)){var b=new Date(a);return isNaN(b)?(k.$setValidity("date",!1),void 0):(k.$setValidity("date",!0),b)}return k.$setValidity("date",!1),void 0}return k.$setValidity("date",!0),null}function n(a,c,d){a&&(h.$watch(b(a),function(a){q[c]=a}),y.attr(d||c,c))}function o(){q.position=s?d.offset(i):d.position(i),q.position.top=q.position.top+i.prop("offsetHeight")}var p,q=h.$new(),r=angular.isDefined(j.closeOnDateSelection)?h.$eval(j.closeOnDateSelection):f.closeOnDateSelection,s=angular.isDefined(j.datepickerAppendToBody)?h.$eval(j.datepickerAppendToBody):f.appendToBody;j.$observe("datepickerPopup",function(a){p=a||f.dateFormat,k.$render()}),q.showButtonBar=angular.isDefined(j.showButtonBar)?h.$eval(j.showButtonBar):f.showButtonBar,h.$on("$destroy",function(){B.remove(),q.$destroy()}),j.$observe("currentText",function(a){q.currentText=angular.isDefined(a)?a:f.currentText}),j.$observe("toggleWeeksText",function(a){q.toggleWeeksText=angular.isDefined(a)?a:f.toggleWeeksText}),j.$observe("clearText",function(a){q.clearText=angular.isDefined(a)?a:f.clearText}),j.$observe("closeText",function(a){q.closeText=angular.isDefined(a)?a:f.closeText});var t,u;j.isOpen&&(t=b(j.isOpen),u=t.assign,h.$watch(t,function(a){q.isOpen=!!a})),q.isOpen=t?t(h):!1;var v=function(a){q.isOpen&&a.target!==i[0]&&q.$apply(function(){l(!1)})},w=function(){q.$apply(function(){l(!0)})},x=angular.element("
");x.attr({"ng-model":"date","ng-change":"dateSelection()"});var y=angular.element(x.children()[0]);j.datepickerOptions&&y.attr(angular.extend({},h.$eval(j.datepickerOptions))),k.$parsers.unshift(m),q.dateSelection=function(a){angular.isDefined(a)&&(q.date=a),k.$setViewValue(q.date),k.$render(),r&&l(!1)},i.bind("input change keyup",function(){q.$apply(function(){q.date=k.$modelValue})}),k.$render=function(){var a=k.$viewValue?e(k.$viewValue,p):"";i.val(a),q.date=k.$modelValue},n(j.min,"min"),n(j.max,"max"),j.showWeeks?n(j.showWeeks,"showWeeks","show-weeks"):(q.showWeeks=g.showWeeks,y.attr("show-weeks","showWeeks")),j.dateDisabled&&y.attr("date-disabled",j.dateDisabled);var z=!1,A=!1;q.$watch("isOpen",function(a){a?(o(),c.bind("click",v),A&&i.unbind("focus",w),i[0].focus(),z=!0):(z&&c.unbind("click",v),i.bind("focus",w),A=!0),u&&u(h,a)}),q.today=function(){q.dateSelection(new Date)},q.clear=function(){q.dateSelection(null)};var B=a(x)(q);s?c.find("body").append(B):i.after(B)}}}]).directive("datepickerPopupWrap",function(){return{restrict:"EA",replace:!0,transclude:!0,templateUrl:"template/datepicker/popup.html",link:function(a,b){b.bind("click",function(a){a.preventDefault(),a.stopPropagation()})}}}),angular.module("ui.bootstrap.dropdownToggle",[]).directive("dropdownToggle",["$document","$location",function(a){var b=null,c=angular.noop;return{restrict:"CA",link:function(d,e){d.$watch("$location.path",function(){c()}),e.parent().bind("click",function(){c()}),e.bind("click",function(d){var f=e===b;d.preventDefault(),d.stopPropagation(),b&&c(),f||e.hasClass("disabled")||e.prop("disabled")||(e.parent().addClass("open"),b=e,c=function(d){d&&(d.preventDefault(),d.stopPropagation()),a.unbind("click",c),e.parent().removeClass("open"),c=angular.noop,b=null},a.bind("click",c))})}}}]),angular.module("ui.bootstrap.modal",[]).factory("$$stackedMap",function(){return{createNew:function(){var a=[];return{add:function(b,c){a.push({key:b,value:c})},get:function(b){for(var c=0;c0),h&&-1==e()&&(h.remove(),h=void 0),d.modalScope.$destroy()}var g,h,i="modal-open",j=c.$new(!0),k=d.createNew(),l={};return c.$watch(e,function(a){j.index=a}),a.bind("keydown",function(a){var b;27===a.which&&(b=k.top(),b&&b.value.keyboard&&c.$apply(function(){l.dismiss(b.key)}))}),l.open=function(c,d){k.add(c,{deferred:d.deferred,modalScope:d.scope,backdrop:d.backdrop,keyboard:d.keyboard});var f=a.find("body").eq(0);e()>=0&&!h&&(g=angular.element("
"),h=b(g)(j),f.append(h));var l=angular.element("
");l.attr("window-class",d.windowClass),l.attr("index",k.length()-1),l.html(d.content);var m=b(l)(d.scope);k.top().value.modalDomEl=m,f.append(m),f.addClass(i)},l.close=function(a,b){var c=k.get(a).value;c&&(c.deferred.resolve(b),f(a))},l.dismiss=function(a,b){var c=k.get(a).value;c&&(c.deferred.reject(b),f(a))},l.getTop=function(){return k.top()},l}]).provider("$modal",function(){var a={options:{backdrop:!0,keyboard:!0},$get:["$injector","$rootScope","$q","$http","$templateCache","$controller","$modalStack",function(b,c,d,e,f,g,h){function i(a){return a.template?d.when(a.template):e.get(a.templateUrl,{cache:f}).then(function(a){return a.data})}function j(a){var c=[];return angular.forEach(a,function(a){(angular.isFunction(a)||angular.isArray(a))&&c.push(d.when(b.invoke(a)))}),c}var k={};return k.open=function(b){var e=d.defer(),f=d.defer(),k={result:e.promise,opened:f.promise,close:function(a){h.close(k,a)},dismiss:function(a){h.dismiss(k,a)}};if(b=angular.extend({},a.options,b),b.resolve=b.resolve||{},!b.template&&!b.templateUrl)throw new Error("One of template or templateUrl options is required.");var l=d.all([i(b)].concat(j(b.resolve)));return l.then(function(a){var d=(b.scope||c).$new();d.$close=k.close,d.$dismiss=k.dismiss;var f,i={},j=1;b.controller&&(i.$scope=d,i.$modalInstance=k,angular.forEach(b.resolve,function(b,c){i[c]=a[j++]}),f=g(b.controller,i)),h.open(k,{scope:d,deferred:e,content:a[0],backdrop:b.backdrop,keyboard:b.keyboard,windowClass:b.windowClass})},function(a){e.reject(a)}),l.then(function(){f.resolve(!0)},function(){f.reject(!1)}),k},k}]};return a}),angular.module("ui.bootstrap.pagination",[]).controller("PaginationController",["$scope","$attrs","$parse","$interpolate",function(a,b,c,d){var e=this,f=b.numPages?c(b.numPages).assign:angular.noop;this.init=function(d){b.itemsPerPage?a.$parent.$watch(c(b.itemsPerPage),function(b){e.itemsPerPage=parseInt(b,10),a.totalPages=e.calculateTotalPages()}):this.itemsPerPage=d},this.noPrevious=function(){return 1===this.page},this.noNext=function(){return this.page===a.totalPages},this.isActive=function(a){return this.page===a},this.calculateTotalPages=function(){var b=this.itemsPerPage<1?1:Math.ceil(a.totalItems/this.itemsPerPage);return Math.max(b||0,1)},this.getAttributeValue=function(b,c,e){return angular.isDefined(b)?e?d(b)(a.$parent):a.$parent.$eval(b):c},this.render=function(){this.page=parseInt(a.page,10)||1,this.page>0&&this.page<=a.totalPages&&(a.pages=this.getPages(this.page,a.totalPages))},a.selectPage=function(b){!e.isActive(b)&&b>0&&b<=a.totalPages&&(a.page=b,a.onSelectPage({page:b}))},a.$watch("page",function(){e.render()}),a.$watch("totalItems",function(){a.totalPages=e.calculateTotalPages()}),a.$watch("totalPages",function(b){f(a.$parent,b),e.page>b?a.selectPage(b):e.render()})}]).constant("paginationConfig",{itemsPerPage:10,boundaryLinks:!1,directionLinks:!0,firstText:"First",previousText:"Previous",nextText:"Next",lastText:"Last",rotate:!0}).directive("pagination",["$parse","paginationConfig",function(a,b){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pagination.html",replace:!0,link:function(c,d,e,f){function g(a,b,c,d){return{number:a,text:b,active:c,disabled:d}}var h,i=f.getAttributeValue(e.boundaryLinks,b.boundaryLinks),j=f.getAttributeValue(e.directionLinks,b.directionLinks),k=f.getAttributeValue(e.firstText,b.firstText,!0),l=f.getAttributeValue(e.previousText,b.previousText,!0),m=f.getAttributeValue(e.nextText,b.nextText,!0),n=f.getAttributeValue(e.lastText,b.lastText,!0),o=f.getAttributeValue(e.rotate,b.rotate);f.init(b.itemsPerPage),e.maxSize&&c.$parent.$watch(a(e.maxSize),function(a){h=parseInt(a,10),f.render()}),f.getPages=function(a,b){var c=[],d=1,e=b,p=angular.isDefined(h)&&b>h;p&&(o?(d=Math.max(a-Math.floor(h/2),1),e=d+h-1,e>b&&(e=b,d=e-h+1)):(d=(Math.ceil(a/h)-1)*h+1,e=Math.min(d+h-1,b)));for(var q=d;e>=q;q++){var r=g(q,q,f.isActive(q),!1);c.push(r)}if(p&&!o){if(d>1){var s=g(d-1,"...",!1,!1);c.unshift(s)}if(b>e){var t=g(e+1,"...",!1,!1);c.push(t)}}if(j){var u=g(a-1,l,!1,f.noPrevious());c.unshift(u);var v=g(a+1,m,!1,f.noNext());c.push(v)}if(i){var w=g(1,k,!1,f.noPrevious());c.unshift(w);var x=g(b,n,!1,f.noNext());c.push(x)}return c}}}}]).constant("pagerConfig",{itemsPerPage:10,previousText:"« Previous",nextText:"Next »",align:!0}).directive("pager",["pagerConfig",function(a){return{restrict:"EA",scope:{page:"=",totalItems:"=",onSelectPage:" &"},controller:"PaginationController",templateUrl:"template/pagination/pager.html",replace:!0,link:function(b,c,d,e){function f(a,b,c,d,e){return{number:a,text:b,disabled:c,previous:i&&d,next:i&&e}}var g=e.getAttributeValue(d.previousText,a.previousText,!0),h=e.getAttributeValue(d.nextText,a.nextText,!0),i=e.getAttributeValue(d.align,a.align);e.init(a.itemsPerPage),e.getPages=function(a){return[f(a-1,g,e.noPrevious(),!0,!1),f(a+1,h,e.noNext(),!1,!0)]}}}}]),angular.module("ui.bootstrap.tooltip",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).provider("$tooltip",function(){function a(a){var b=/[A-Z]/g,c="-";return a.replace(b,function(a,b){return(b?c:"")+a.toLowerCase()})}var b={placement:"top",animation:!0,popupDelay:0},c={mouseenter:"mouseleave",click:"click",focus:"blur"},d={};this.options=function(a){angular.extend(d,a)},this.setTriggers=function(a){angular.extend(c,a)},this.$get=["$window","$compile","$timeout","$parse","$document","$position","$interpolate",function(e,f,g,h,i,j,k){return function(e,l,m){function n(a){var b=a||o.trigger||m,d=c[b]||b;return{show:b,hide:d}}var o=angular.extend({},b,d),p=a(e),q=k.startSymbol(),r=k.endSymbol(),s="
';return{restrict:"EA",scope:!0,link:function(a,b,c){function d(){a.tt_isOpen?m():k()}function k(){(!y||a.$eval(c[l+"Enable"]))&&(a.tt_popupDelay?(t=g(p,a.tt_popupDelay),t.then(function(a){a()})):a.$apply(p)())}function m(){a.$apply(function(){q()})}function p(){return a.tt_content?(r&&g.cancel(r),u.css({top:0,left:0,display:"block"}),v?i.find("body").append(u):b.after(u),z(),a.tt_isOpen=!0,z):angular.noop}function q(){a.tt_isOpen=!1,g.cancel(t),a.tt_animation?r=g(function(){u.remove()},500):u.remove()}var r,t,u=f(s)(a),v=angular.isDefined(o.appendToBody)?o.appendToBody:!1,w=n(void 0),x=!1,y=angular.isDefined(c[l+"Enable"]),z=function(){var c,d,e,f;switch(c=v?j.offset(b):j.position(b),d=u.prop("offsetWidth"),e=u.prop("offsetHeight"),a.tt_placement){case"right":f={top:c.top+c.height/2-e/2,left:c.left+c.width};break;case"bottom":f={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"left":f={top:c.top+c.height/2-e/2,left:c.left-d};break;default:f={top:c.top-e,left:c.left+c.width/2-d/2}}f.top+="px",f.left+="px",u.css(f)};a.tt_isOpen=!1,c.$observe(e,function(b){a.tt_content=b,!b&&a.tt_isOpen&&q()}),c.$observe(l+"Title",function(b){a.tt_title=b}),c.$observe(l+"Placement",function(b){a.tt_placement=angular.isDefined(b)?b:o.placement}),c.$observe(l+"PopupDelay",function(b){var c=parseInt(b,10);a.tt_popupDelay=isNaN(c)?o.popupDelay:c});var A=function(){x&&(b.unbind(w.show,k),b.unbind(w.hide,m))};c.$observe(l+"Trigger",function(a){A(),w=n(a),w.show===w.hide?b.bind(w.show,d):(b.bind(w.show,k),b.bind(w.hide,m)),x=!0});var B=a.$eval(c[l+"Animation"]);a.tt_animation=angular.isDefined(B)?!!B:o.animation,c.$observe(l+"AppendToBody",function(b){v=angular.isDefined(b)?h(b)(a):v}),v&&a.$on("$locationChangeSuccess",function(){a.tt_isOpen&&q()}),a.$on("$destroy",function(){g.cancel(r),g.cancel(t),A(),u.remove(),u.unbind(),u=null})}}}}]}).directive("tooltipPopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-popup.html"}}).directive("tooltip",["$tooltip",function(a){return a("tooltip","tooltip","mouseenter")}]).directive("tooltipHtmlUnsafePopup",function(){return{restrict:"EA",replace:!0,scope:{content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/tooltip/tooltip-html-unsafe-popup.html"}}).directive("tooltipHtmlUnsafe",["$tooltip",function(a){return a("tooltipHtmlUnsafe","tooltip","mouseenter")}]),angular.module("ui.bootstrap.popover",["ui.bootstrap.tooltip"]).directive("popoverPopup",function(){return{restrict:"EA",replace:!0,scope:{title:"@",content:"@",placement:"@",animation:"&",isOpen:"&"},templateUrl:"template/popover/popover.html"}}).directive("popover",["$compile","$timeout","$parse","$window","$tooltip",function(a,b,c,d,e){return e("popover","popover","click")}]),angular.module("ui.bootstrap.progressbar",["ui.bootstrap.transition"]).constant("progressConfig",{animate:!0,max:100}).controller("ProgressController",["$scope","$attrs","progressConfig","$transition",function(a,b,c,d){var e=this,f=[],g=angular.isDefined(b.max)?a.$parent.$eval(b.max):c.max,h=angular.isDefined(b.animate)?a.$parent.$eval(b.animate):c.animate;this.addBar=function(a,b){var c=0,d=a.$parent.$index;angular.isDefined(d)&&f[d]&&(c=f[d].value),f.push(a),this.update(b,a.value,c),a.$watch("value",function(a,c){a!==c&&e.update(b,a,c)}),a.$on("$destroy",function(){e.removeBar(a)})},this.update=function(a,b,c){var e=this.getPercentage(b);h?(a.css("width",this.getPercentage(c)+"%"),d(a,{width:e+"%"})):a.css({transition:"none",width:e+"%"})},this.removeBar=function(a){f.splice(f.indexOf(a),1)},this.getPercentage=function(a){return Math.round(100*a/g)}}]).directive("progress",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",require:"progress",scope:{},template:'
'}}).directive("bar",function(){return{restrict:"EA",replace:!0,transclude:!0,require:"^progress",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/bar.html",link:function(a,b,c,d){d.addBar(a,b)}}}).directive("progressbar",function(){return{restrict:"EA",replace:!0,transclude:!0,controller:"ProgressController",scope:{value:"=",type:"@"},templateUrl:"template/progressbar/progressbar.html",link:function(a,b,c,d){d.addBar(a,angular.element(b.children()[0]))}}}),angular.module("ui.bootstrap.rating",[]).constant("ratingConfig",{max:5,stateOn:null,stateOff:null}).controller("RatingController",["$scope","$attrs","$parse","ratingConfig",function(a,b,c,d){this.maxRange=angular.isDefined(b.max)?a.$parent.$eval(b.max):d.max,this.stateOn=angular.isDefined(b.stateOn)?a.$parent.$eval(b.stateOn):d.stateOn,this.stateOff=angular.isDefined(b.stateOff)?a.$parent.$eval(b.stateOff):d.stateOff,this.createRateObjects=function(a){for(var b={stateOn:this.stateOn,stateOff:this.stateOff},c=0,d=a.length;d>c;c++)a[c]=angular.extend({index:c},b,a[c]);return a},a.range=angular.isDefined(b.ratingStates)?this.createRateObjects(angular.copy(a.$parent.$eval(b.ratingStates))):this.createRateObjects(new Array(this.maxRange)),a.rate=function(b){a.readonly||a.value===b||(a.value=b)},a.enter=function(b){a.readonly||(a.val=b),a.onHover({value:b})},a.reset=function(){a.val=angular.copy(a.value),a.onLeave()},a.$watch("value",function(b){a.val=b}),a.readonly=!1,b.readonly&&a.$parent.$watch(c(b.readonly),function(b){a.readonly=!!b})}]).directive("rating",function(){return{restrict:"EA",scope:{value:"=",onHover:"&",onLeave:"&"},controller:"RatingController",templateUrl:"template/rating/rating.html",replace:!0}}),angular.module("ui.bootstrap.tabs",[]).controller("TabsetController",["$scope",function(a){var b=this,c=b.tabs=a.tabs=[]; +b.select=function(a){angular.forEach(c,function(a){a.active=!1}),a.active=!0},b.addTab=function(a){c.push(a),(1===c.length||a.active)&&b.select(a)},b.removeTab=function(a){var d=c.indexOf(a);if(a.active&&c.length>1){var e=d==c.length-1?d-1:d+1;b.select(c[e])}c.splice(d,1)}}]).directive("tabset",function(){return{restrict:"EA",transclude:!0,replace:!0,scope:{},controller:"TabsetController",templateUrl:"template/tabs/tabset.html",link:function(a,b,c){a.vertical=angular.isDefined(c.vertical)?a.$parent.$eval(c.vertical):!1,a.justified=angular.isDefined(c.justified)?a.$parent.$eval(c.justified):!1,a.type=angular.isDefined(c.type)?a.$parent.$eval(c.type):"tabs"}}}).directive("tab",["$parse",function(a){return{require:"^tabset",restrict:"EA",replace:!0,templateUrl:"template/tabs/tab.html",transclude:!0,scope:{heading:"@",onSelect:"&select",onDeselect:"&deselect"},controller:function(){},compile:function(b,c,d){return function(b,c,e,f){var g,h;e.active?(g=a(e.active),h=g.assign,b.$parent.$watch(g,function(a,c){a!==c&&(b.active=!!a)}),b.active=g(b.$parent)):h=g=angular.noop,b.$watch("active",function(a){h(b.$parent,a),a?(f.select(b),b.onSelect()):b.onDeselect()}),b.disabled=!1,e.disabled&&b.$parent.$watch(a(e.disabled),function(a){b.disabled=!!a}),b.select=function(){b.disabled||(b.active=!0)},f.addTab(b),b.$on("$destroy",function(){f.removeTab(b)}),b.$transcludeFn=d}}}}]).directive("tabHeadingTransclude",[function(){return{restrict:"A",require:"^tab",link:function(a,b){a.$watch("headingElement",function(a){a&&(b.html(""),b.append(a))})}}}]).directive("tabContentTransclude",function(){function a(a){return a.tagName&&(a.hasAttribute("tab-heading")||a.hasAttribute("data-tab-heading")||"tab-heading"===a.tagName.toLowerCase()||"data-tab-heading"===a.tagName.toLowerCase())}return{restrict:"A",require:"^tabset",link:function(b,c,d){var e=b.$eval(d.tabContentTransclude);e.$transcludeFn(e.$parent,function(b){angular.forEach(b,function(b){a(b)?e.headingElement=b:c.append(b)})})}}}),angular.module("ui.bootstrap.timepicker",[]).constant("timepickerConfig",{hourStep:1,minuteStep:1,showMeridian:!0,meridians:null,readonlyInput:!1,mousewheel:!0}).directive("timepicker",["$parse","$log","timepickerConfig","$locale",function(a,b,c,d){return{restrict:"EA",require:"?^ngModel",replace:!0,scope:{},templateUrl:"template/timepicker/timepicker.html",link:function(e,f,g,h){function i(){var a=parseInt(e.hours,10),b=e.showMeridian?a>0&&13>a:a>=0&&24>a;return b?(e.showMeridian&&(12===a&&(a=0),e.meridian===q[1]&&(a+=12)),a):void 0}function j(){var a=parseInt(e.minutes,10);return a>=0&&60>a?a:void 0}function k(a){return angular.isDefined(a)&&a.toString().length<2?"0"+a:a}function l(a){m(),h.$setViewValue(new Date(p)),n(a)}function m(){h.$setValidity("time",!0),e.invalidHours=!1,e.invalidMinutes=!1}function n(a){var b=p.getHours(),c=p.getMinutes();e.showMeridian&&(b=0===b||12===b?12:b%12),e.hours="h"===a?b:k(b),e.minutes="m"===a?c:k(c),e.meridian=p.getHours()<12?q[0]:q[1]}function o(a){var b=new Date(p.getTime()+6e4*a);p.setHours(b.getHours(),b.getMinutes()),l()}if(h){var p=new Date,q=angular.isDefined(g.meridians)?e.$parent.$eval(g.meridians):c.meridians||d.DATETIME_FORMATS.AMPMS,r=c.hourStep;g.hourStep&&e.$parent.$watch(a(g.hourStep),function(a){r=parseInt(a,10)});var s=c.minuteStep;g.minuteStep&&e.$parent.$watch(a(g.minuteStep),function(a){s=parseInt(a,10)}),e.showMeridian=c.showMeridian,g.showMeridian&&e.$parent.$watch(a(g.showMeridian),function(a){if(e.showMeridian=!!a,h.$error.time){var b=i(),c=j();angular.isDefined(b)&&angular.isDefined(c)&&(p.setHours(b),l())}else n()});var t=f.find("input"),u=t.eq(0),v=t.eq(1),w=angular.isDefined(g.mousewheel)?e.$eval(g.mousewheel):c.mousewheel;if(w){var x=function(a){a.originalEvent&&(a=a.originalEvent);var b=a.wheelDelta?a.wheelDelta:-a.deltaY;return a.detail||b>0};u.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementHours():e.decrementHours()),a.preventDefault()}),v.bind("mousewheel wheel",function(a){e.$apply(x(a)?e.incrementMinutes():e.decrementMinutes()),a.preventDefault()})}if(e.readonlyInput=angular.isDefined(g.readonlyInput)?e.$eval(g.readonlyInput):c.readonlyInput,e.readonlyInput)e.updateHours=angular.noop,e.updateMinutes=angular.noop;else{var y=function(a,b){h.$setViewValue(null),h.$setValidity("time",!1),angular.isDefined(a)&&(e.invalidHours=a),angular.isDefined(b)&&(e.invalidMinutes=b)};e.updateHours=function(){var a=i();angular.isDefined(a)?(p.setHours(a),l("h")):y(!0)},u.bind("blur",function(){!e.validHours&&e.hours<10&&e.$apply(function(){e.hours=k(e.hours)})}),e.updateMinutes=function(){var a=j();angular.isDefined(a)?(p.setMinutes(a),l("m")):y(void 0,!0)},v.bind("blur",function(){!e.invalidMinutes&&e.minutes<10&&e.$apply(function(){e.minutes=k(e.minutes)})})}h.$render=function(){var a=h.$modelValue?new Date(h.$modelValue):null;isNaN(a)?(h.$setValidity("time",!1),b.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.')):(a&&(p=a),m(),n())},e.incrementHours=function(){o(60*r)},e.decrementHours=function(){o(60*-r)},e.incrementMinutes=function(){o(s)},e.decrementMinutes=function(){o(-s)},e.toggleMeridian=function(){o(720*(p.getHours()<12?1:-1))}}}}}]),angular.module("ui.bootstrap.typeahead",["ui.bootstrap.position","ui.bootstrap.bindHtml"]).factory("typeaheadParser",["$parse",function(a){var b=/^\s*(.*?)(?:\s+as\s+(.*?))?\s+for\s+(?:([\$\w][\$\w\d]*))\s+in\s+(.*)$/;return{parse:function(c){var d=c.match(b);if(!d)throw new Error("Expected typeahead specification in form of '_modelValue_ (as _label_)? for _item_ in _collection_' but got '"+c+"'.");return{itemName:d[3],source:a(d[4]),viewMapper:a(d[2]||d[1]),modelMapper:a(d[1])}}}}]).directive("typeahead",["$compile","$parse","$q","$timeout","$document","$position","typeaheadParser",function(a,b,c,d,e,f,g){var h=[9,13,27,38,40];return{require:"ngModel",link:function(i,j,k,l){var m,n=i.$eval(k.typeaheadMinLength)||1,o=i.$eval(k.typeaheadWaitMs)||0,p=i.$eval(k.typeaheadEditable)!==!1,q=b(k.typeaheadLoading).assign||angular.noop,r=b(k.typeaheadOnSelect),s=k.typeaheadInputFormatter?b(k.typeaheadInputFormatter):void 0,t=k.typeaheadAppendToBody?b(k.typeaheadAppendToBody):!1,u=b(k.ngModel).assign,v=g.parse(k.typeahead),w=angular.element("
");w.attr({matches:"matches",active:"activeIdx",select:"select(activeIdx)",query:"query",position:"position"}),angular.isDefined(k.typeaheadTemplateUrl)&&w.attr("template-url",k.typeaheadTemplateUrl);var x=i.$new();i.$on("$destroy",function(){x.$destroy()});var y=function(){x.matches=[],x.activeIdx=-1},z=function(a){var b={$viewValue:a};q(i,!0),c.when(v.source(i,b)).then(function(c){if(a===l.$viewValue&&m){if(c.length>0){x.activeIdx=0,x.matches.length=0;for(var d=0;d=n?o>0?(A&&d.cancel(A),A=d(function(){z(a)},o)):z(a):(q(i,!1),y()),p?a:a?(l.$setValidity("editable",!1),void 0):(l.$setValidity("editable",!0),a)}),l.$formatters.push(function(a){var b,c,d={};return s?(d.$model=a,s(i,d)):(d[v.itemName]=a,b=v.viewMapper(i,d),d[v.itemName]=void 0,c=v.viewMapper(i,d),b!==c?b:a)}),x.select=function(a){var b,c,d={};d[v.itemName]=c=x.matches[a].model,b=v.modelMapper(i,d),u(i,b),l.$setValidity("editable",!0),r(i,{$item:c,$model:b,$label:v.viewMapper(i,d)}),y(),j[0].focus()},j.bind("keydown",function(a){0!==x.matches.length&&-1!==h.indexOf(a.which)&&(a.preventDefault(),40===a.which?(x.activeIdx=(x.activeIdx+1)%x.matches.length,x.$digest()):38===a.which?(x.activeIdx=(x.activeIdx?x.activeIdx:x.matches.length)-1,x.$digest()):13===a.which||9===a.which?x.$apply(function(){x.select(x.activeIdx)}):27===a.which&&(a.stopPropagation(),y(),x.$digest()))}),j.bind("blur",function(){m=!1});var B=function(a){j[0]!==a.target&&(y(),x.$digest())};e.bind("click",B),i.$on("$destroy",function(){e.unbind("click",B)});var C=a(w)(x);t?e.find("body").append(C):j.after(C)}}}]).directive("typeaheadPopup",function(){return{restrict:"EA",scope:{matches:"=",query:"=",active:"=",position:"=",select:"&"},replace:!0,templateUrl:"template/typeahead/typeahead-popup.html",link:function(a,b,c){a.templateUrl=c.templateUrl,a.isOpen=function(){return a.matches.length>0},a.isActive=function(b){return a.active==b},a.selectActive=function(b){a.active=b},a.selectMatch=function(b){a.select({activeIdx:b})}}}}).directive("typeaheadMatch",["$http","$templateCache","$compile","$parse",function(a,b,c,d){return{restrict:"EA",scope:{index:"=",match:"=",query:"="},link:function(e,f,g){var h=d(g.templateUrl)(e.$parent)||"template/typeahead/typeahead-match.html";a.get(h,{cache:b}).success(function(a){f.replaceWith(c(a.trim())(e))})}}}]).filter("typeaheadHighlight",function(){function a(a){return a.replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")}return function(b,c){return c?b.replace(new RegExp(a(c),"gi"),"$&"):b}}),angular.module("template/accordion/accordion-group.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion-group.html",'
\n
\n

\n {{heading}}\n

\n
\n
\n
\n
\n
')}]),angular.module("template/accordion/accordion.html",[]).run(["$templateCache",function(a){a.put("template/accordion/accordion.html",'
')}]),angular.module("template/alert/alert.html",[]).run(["$templateCache",function(a){a.put("template/alert/alert.html","
\n \n
\n
\n")}]),angular.module("template/carousel/carousel.html",[]).run(["$templateCache",function(a){a.put("template/carousel/carousel.html",'\n')}]),angular.module("template/carousel/slide.html",[]).run(["$templateCache",function(a){a.put("template/carousel/slide.html","
\n")}]),angular.module("template/datepicker/datepicker.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/datepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
#{{label}}
{{ getWeekNumber(row) }}\n \n
\n')}]),angular.module("template/datepicker/popup.html",[]).run(["$templateCache",function(a){a.put("template/datepicker/popup.html","
    \n
  • \n"+'
  • \n \n \n \n \n \n \n
  • \n
\n')}]),angular.module("template/modal/backdrop.html",[]).run(["$templateCache",function(a){a.put("template/modal/backdrop.html",'')}]),angular.module("template/modal/window.html",[]).run(["$templateCache",function(a){a.put("template/modal/window.html",'')}]),angular.module("template/pagination/pager.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pager.html",'')}]),angular.module("template/pagination/pagination.html",[]).run(["$templateCache",function(a){a.put("template/pagination/pagination.html",'')}]),angular.module("template/tooltip/tooltip-html-unsafe-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-html-unsafe-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/tooltip/tooltip-popup.html",[]).run(["$templateCache",function(a){a.put("template/tooltip/tooltip-popup.html",'
\n
\n
\n
\n')}]),angular.module("template/popover/popover.html",[]).run(["$templateCache",function(a){a.put("template/popover/popover.html",'
\n
\n\n
\n

\n
\n
\n
\n')}]),angular.module("template/progressbar/bar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/bar.html",'
')}]),angular.module("template/progressbar/progress.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progress.html",'
')}]),angular.module("template/progressbar/progressbar.html",[]).run(["$templateCache",function(a){a.put("template/progressbar/progressbar.html",'
')}]),angular.module("template/rating/rating.html",[]).run(["$templateCache",function(a){a.put("template/rating/rating.html",'\n \n')}]),angular.module("template/tabs/tab.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tab.html",'
  • \n {{heading}}\n
  • \n')}]),angular.module("template/tabs/tabset-titles.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset-titles.html","
      \n
    \n")}]),angular.module("template/tabs/tabset.html",[]).run(["$templateCache",function(a){a.put("template/tabs/tabset.html",'\n
    \n \n
    \n
    \n
    \n
    \n
    \n')}]),angular.module("template/timepicker/timepicker.html",[]).run(["$templateCache",function(a){a.put("template/timepicker/timepicker.html",'\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
     
    \n \n :\n \n
     
    \n')}]),angular.module("template/typeahead/typeahead-match.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-match.html",'')}]),angular.module("template/typeahead/typeahead-popup.html",[]).run(["$templateCache",function(a){a.put("template/typeahead/typeahead-popup.html","
      \n"+'
    • \n
      \n
    • \n
    ')}]); \ No newline at end of file diff --git a/frontend/app/views/create goal/date-validity-goal.html b/frontend/app/views/create goal/date-validity-goal.html index 0e533eb..fb07eb6 100644 --- a/frontend/app/views/create goal/date-validity-goal.html +++ b/frontend/app/views/create goal/date-validity-goal.html @@ -1,7 +1,8 @@
    -
    +
    {{(goalCtrl.goal.timeBox.startDate|date)}} - {{(goalCtrl.goal.timeBox.endDate|date)}}
    -
    \ No newline at end of file + diff --git a/frontend/app/views/create-goal.html b/frontend/app/views/create-goal.html index 7202db6..e2dff6a 100644 --- a/frontend/app/views/create-goal.html +++ b/frontend/app/views/create-goal.html @@ -1,20 +1,28 @@ -
    - - -
    - - -
    +
    + - + -
    -
    + + -
    +
    +
    +

    Date du début de la validité de l'objectif

    + +
    +
    +

    Date de la fin de la validité de l'objectif

    + +
    +
    +
    @@ -22,7 +30,9 @@

    Créer un objectif

    - + @@ -31,31 +41,31 @@

    Créer un objectif

    -->
    -
    +
    -
    -
    +
    +
    -
      - +
        + -
      • -
        +
      • +
        -
        -
      • +
    + - + -
  • -
    - -
    -
  • - +
  • +
    + +
    +
  • + - +
    diff --git a/frontend/bower.json b/frontend/bower.json index 2f6bd82..09a56bd 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -3,11 +3,12 @@ "version": "0.0.0", "dependencies": { "angular": "^1.3.0", - "bootstrap": "^3.2.0", + "bootstrap": "3.1.1", "angular-cookies": "^1.3.0", "angular-resource": "^1.3.0", "angular-route": "^1.3.0", "angular-sanitize": "^1.3.0", + "angular-animate": "^1.3.0", "angular-touch": "^1.3.0" }, "devDependencies": { diff --git a/frontend/test/karma.conf.js b/frontend/test/karma.conf.js index dd1cda0..36dffe7 100644 --- a/frontend/test/karma.conf.js +++ b/frontend/test/karma.conf.js @@ -29,6 +29,7 @@ module.exports = function(config) { 'bower_components/angular-resource/angular-resource.js', 'bower_components/angular-route/angular-route.js', 'bower_components/angular-sanitize/angular-sanitize.js', + 'bower_components/angular-animate/angular-animate.js', 'bower_components/angular-touch/angular-touch.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower From 9f24d800f035f897af21e033b48cfabe9d2f10df Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 13:29:54 +0200 Subject: [PATCH 22/28] Create goal is OP :+1: Add badgeRouter to get all badges from create-goal page Add goalRouter to handle the creation of a new goal update Backend to open routes update frontend controllers and views --- backend/src/Backend.ts | 6 +- backend/src/api/BadgeRouter.ts | 98 +++++++++++++++++++ backend/src/api/GoalRouter.ts | 51 ++++++++++ .../app/scripts/controllers/ServiceGoal.js | 2 +- frontend/app/scripts/controllers/goal.js | 27 +++-- .../app/views/create goal/comparison.html | 12 +-- frontend/app/views/create-goal.html | 27 ++--- 7 files changed, 193 insertions(+), 30 deletions(-) create mode 100644 backend/src/api/BadgeRouter.ts create mode 100644 backend/src/api/GoalRouter.ts diff --git a/backend/src/Backend.ts b/backend/src/Backend.ts index d9c6505..b8deffa 100644 --- a/backend/src/Backend.ts +++ b/backend/src/Backend.ts @@ -6,6 +6,8 @@ import Server = require('./Server'); import DashboardRouter = require('./api/DashboardRouter'); import LoginRouter = require('./api/LoginRouter'); +import BadgeRouter = require('./api/BadgeRouter'); +import GoalRouter = require('./api/GoalRouter'); import Context = require('./Context'); @@ -46,9 +48,9 @@ class Backend extends Server { this.app.use('/dashboard', (new DashboardRouter(self.context, new Middleware())).getRouter()); this.app.use('/login', (new LoginRouter(self.context)).getRouter()); + this.app.use("/badges", (new BadgeRouter(self.context)).getRouter()); + this.app.use("/goals", (new GoalRouter(self.context)).getRouter()); /* - this.app.use("/badges", (new BadgeRouter(self.badgeRepository, self.badgeFactory, self.userRepository, loginCheck)).getRouter()); - this.app.use("/goals", (new GoalDefinitionRouter(self.goalDefinitionRepository, self.goalDefinitionFactory, self.challengeRepository, self.userRepository)).getRouter()); this.app.use("/challenges", (new GoalInstanceRouter(self.challengeRepository, self.challengeFactory, self.goalDefinitionRepository, self.userRepository)).getRouter()); */ diff --git a/backend/src/api/BadgeRouter.ts b/backend/src/api/BadgeRouter.ts new file mode 100644 index 0000000..3a69600 --- /dev/null +++ b/backend/src/api/BadgeRouter.ts @@ -0,0 +1,98 @@ + +import RouterItf = require('./RouterItf'); + +import BadgeRepository = require('../badge/BadgeRepository'); +import BadgeFactory = require('../badge/BadgeFactory'); +import UserRepository = require('../user/UserRepository'); +import Context = require('../Context'); + +import BadArgumentException = require('../exceptions/BadArgumentException'); + +/** + * BadgeRouter class
    + * This class handle all the API for + * badge class and related. + * + * @class BadgeRouter + * @extends RouterItf + */ +class BadgeRouter extends RouterItf { + + private userRepository:UserRepository; + private badgeRepository:BadgeRepository; + private badgeFactory:BadgeFactory; + + /** + * Constructor : will take the specified badgeRepository + * and init the embedded badge factory. + * @param badgeRepository + * The badge repository to save and retrieve badges + */ + constructor(context:Context) { + super(); + this.badgeRepository = context.getBadgeRepository(); + this.badgeFactory = context.getBadgeFactory(); + this.userRepository = context.getUserRepository(); + } + + buildRouter() { + var self = this; + + this.router.post('/new', function(req,res) { + self.newBadge(req,res); + }); + + this.router.get('/all', function(req,res) { + self.getAllBadges(req,res); + }); + + this.router.get('/:id', function(req, res) { + self.getBadge(req, res); + }); + } + + /** + * This method will return all badges + * using badgeRepository#getAllBadges + * @param req + * @param res + */ + getAllBadges(req:any, res:any) { + var badges = this.badgeRepository.getDataInJSON(); + res.send(badges); + } + + /** + * This method will return a specific badge + * using badgeRepository#getBadge + * @param req + * @param res + */ + getBadge(req:any, res:any) { + var badge = this.badgeRepository.getBadge(req.params.id).getDataInJSON(); + res.send(badge); + } + + /** + * This method will create the badge via + * its internal badge factory and will add it + * into the specified badge repository
    + * See badgeFactory#createBadge method to + * see required request fields + * @param req + * @param res + */ + newBadge(req:any, res:any) { + var badgeData = req.body; + + try { + var badge = this.badgeFactory.createBadge(badgeData); + this.badgeRepository.addBadge(badge); + res.send({success:'Badge successfully created', info:badge.getUuid(), description:badge.getDataInJSON()}); + } catch (e) { + res.status(400).send({'error': e.toString()}); + } + } +} + +export = BadgeRouter; \ No newline at end of file diff --git a/backend/src/api/GoalRouter.ts b/backend/src/api/GoalRouter.ts new file mode 100644 index 0000000..d4d479c --- /dev/null +++ b/backend/src/api/GoalRouter.ts @@ -0,0 +1,51 @@ +import RouterItf = require('./RouterItf'); + +import GoalRepository = require('../goal/GoalRepository'); +import GoalFactory = require('../goal/GoalFactory'); +import Context = require('../Context'); + +/** + * GoalDefinitionRouter class
    + * This class handle all the API for + * goal definition class and related. + * + * @class GoalDefinitionRouter + * @extends RouterItf + */ + +class GoalRouter extends RouterItf { + + private goalRepository:GoalRepository; + private goalFactory:GoalFactory; + + + constructor(context:Context) { + super(); + this.goalRepository = context.getGoalRepository(); + this.goalFactory = context.getGoalFactory(); + + } + + buildRouter() { + var self = this; + + this.router.post('/new', function (req, res) { + self.addGoalDefinition(req, res); + }); + } + + addGoalDefinition(req:any, res:any) { + var data = req.body; + try { + var newGoal = this.goalFactory.createGoal(data); + this.goalRepository.addGoal(newGoal); + res.send("OK : définition d'objectif créee avec succès"); + } + catch (e) { + console.log("err", e); + res.send(e.toString()); + } + } +} + +export = GoalRouter; \ No newline at end of file diff --git a/frontend/app/scripts/controllers/ServiceGoal.js b/frontend/app/scripts/controllers/ServiceGoal.js index 02aa5af..8277349 100644 --- a/frontend/app/scripts/controllers/ServiceGoal.js +++ b/frontend/app/scripts/controllers/ServiceGoal.js @@ -37,7 +37,7 @@ app.service('ServiceGoal', ['$http', function ServiceGoal($http) { this.post = function (goal, successFunc, failFunc) { var path = basePathGoal + 'new'; - console.log('Service Goal : Get On', path); + console.log('Service Goal : Post On', path); $http.post(path, goal) .success(function () { diff --git a/frontend/app/scripts/controllers/goal.js b/frontend/app/scripts/controllers/goal.js index 85528b3..f6fe11e 100644 --- a/frontend/app/scripts/controllers/goal.js +++ b/frontend/app/scripts/controllers/goal.js @@ -7,9 +7,9 @@ var app = angular.module('ecoknowledgeApp') self.goal = {}; self.goal.conditions = []; self.goal.name = ''; - self.goal.timeBox = {}; - self.goal.timeBox.startDate = new Date().getTime(); - self.goal.timeBox.endDate = new Date().getTime(); + self.goal.validityPeriod = {}; + self.goal.validityPeriod.start = new Date().getTime(); + self.goal.validityPeriod.end = new Date().getTime(); self.badges = []; self.selectedBadge = null; @@ -34,7 +34,7 @@ var app = angular.module('ecoknowledgeApp') }; this.changeType = function(iteration, type){ - iteration.sensor = (type==='sensor'); + iteration.symbolicName = (type==='sensor'); iteration.value = null; }; @@ -91,13 +91,20 @@ var app = angular.module('ecoknowledgeApp') type:'number', valueLeft:{ value:null, - sensor:true + symbolicName:true }, valueRight:{ value:null, - sensor:false - }, - description:null + symbolicName:false + } + }, + filter: { + dayOfWeekFilter: 'all', + periodOfDayFilter : ['all'] + }, + referencePeriod:{ + numberOfUnitToSubtract:1, + unitToSubtract:null } }; }; @@ -116,6 +123,10 @@ var app = angular.module('ecoknowledgeApp') }else if(iteration.threshold>100) { iteration.threshold = 100; } + + if(iteration.type == 'comparison') { + iteration.expression.valueRight.value = iteration.threshold; + } }; self.week = function(){ diff --git a/frontend/app/views/create goal/comparison.html b/frontend/app/views/create goal/comparison.html index 6a37179..02402de 100644 --- a/frontend/app/views/create goal/comparison.html +++ b/frontend/app/views/create goal/comparison.html @@ -9,13 +9,13 @@ - - - - + + + - {{iteration.expression.periodOfTime}} + {{iteration.referencePeriod.unitToSubtract}}
    - \ No newline at end of file + diff --git a/frontend/app/views/create-goal.html b/frontend/app/views/create-goal.html index e2dff6a..894fba3 100644 --- a/frontend/app/views/create-goal.html +++ b/frontend/app/views/create-goal.html @@ -11,27 +11,28 @@

    Créer un objectif

    ng-model="goalCtrl.goal.name" required>
    -
    -

    Date du début de la validité de l'objectif

    - -
    -
    -

    Date de la fin de la validité de l'objectif

    - -
    +
    +

    Date du début de la validité de l'objectif

    + +
    +
    +

    Date de la fin de la validité de l'objectif

    + +
    - - From 8e497a81844d71d4e0452378d04ba9957aecb0b5 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 13:53:01 +0200 Subject: [PATCH 23/28] Delete obsolete front end test --- .travis.yml | 1 - frontend/Gruntfile.js | 2 + .../spec/controllers/ServiceChallengeTest.js | 79 --------------- .../test/spec/controllers/ServiceGoalTest.js | 96 ------------------- frontend/test/spec/controllers/about.js | 22 ----- frontend/test/spec/controllers/main.js | 22 ----- 6 files changed, 2 insertions(+), 220 deletions(-) delete mode 100644 frontend/test/spec/controllers/ServiceChallengeTest.js delete mode 100644 frontend/test/spec/controllers/ServiceGoalTest.js delete mode 100644 frontend/test/spec/controllers/about.js delete mode 100644 frontend/test/spec/controllers/main.js diff --git a/.travis.yml b/.travis.yml index d9ca353..8a9dbc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,3 @@ before_script: - 'cd ../frontend' - 'npm install' - 'bower install' - - 'grunt test' diff --git a/frontend/Gruntfile.js b/frontend/Gruntfile.js index 8189812..8a0ebdf 100644 --- a/frontend/Gruntfile.js +++ b/frontend/Gruntfile.js @@ -410,6 +410,7 @@ module.exports = function (grunt) { grunt.task.run(['serve:' + target]); }); + /* grunt.registerTask('test', [ 'clean:server', 'wiredep', @@ -418,6 +419,7 @@ module.exports = function (grunt) { 'connect:test', 'karma' ]); +*/ grunt.registerTask('build', [ 'clean:dist', diff --git a/frontend/test/spec/controllers/ServiceChallengeTest.js b/frontend/test/spec/controllers/ServiceChallengeTest.js deleted file mode 100644 index abc0470..0000000 --- a/frontend/test/spec/controllers/ServiceChallengeTest.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -var basePath = 'http://localhost:3000/challenges/'; - -describe('Service: ServiceChallenge', function() { - var Challenge, httpBackend; - - //load the service's module - beforeEach(module('ecoknowledgeApp')); - - //Initialize the controller and a mock Scope - beforeEach(inject(function (_$httpBackend_, _ServiceChallenge_) { - httpBackend = _$httpBackend_; - Challenge = _ServiceChallenge_; - })); - - describe('Challenge.get', function () { - var callbacks; - - beforeEach(function () { - callbacks = { - success: function () { - }, - error: function () { - } - }; - spyOn(callbacks, 'success').and.callThrough(); - spyOn(callbacks, 'error').and.callThrough(); - }); - - - it('should get all the Challenges from the service', function () { - var mockMonChallenge = { goal: 'Mon goal', points: '37', description: 'Une description', name: 'MonChallenge' }; - var mockMonChallengePatrick = { goal: 'Mon goal de fou', points: '42', description: 'Un de scription', name: 'MonChallengePatrick' }; - var mockResult = [mockMonChallenge, mockMonChallengePatrick]; - //backend definition returns a mock user - var urlPath = basePath+'all/'; - - httpBackend.when('GET',urlPath).respond(mockResult); - httpBackend.expectGET(urlPath); - Challenge.get('', callbacks.success, callbacks.error); - - httpBackend.flush(); - - expect(callbacks.success).toHaveBeenCalled(); - expect(callbacks.error).not.toHaveBeenCalled(); - expect(callbacks.success.calls.argsFor(0)).toEqual([mockResult]); - }); - }); - - describe('Challenge.post', function () { - var callbacks; - - beforeEach(function () { - callbacks = { - success: function () { - }, - error: function () { - } - }; - spyOn(callbacks, 'success').and.callThrough(); - spyOn(callbacks, 'error').and.callThrough(); - }); - - it('should send a new goal to the service', function(){ - var mockMonChallengePatrick = { goal: 'Mon goal de fou', points: '42', description: 'Un de scription', name: 'MonChallengePatrick' }; - - httpBackend.when('POST',basePath +'new'); - httpBackend.expectPOST(basePath+'new').respond('gg wp'); - Challenge.post(mockMonChallengePatrick, callbacks.success, callbacks.error); - - httpBackend.flush(); - - expect(callbacks.success).toHaveBeenCalled(); - expect(callbacks.error).not.toHaveBeenCalled(); - }); - - }); -}); diff --git a/frontend/test/spec/controllers/ServiceGoalTest.js b/frontend/test/spec/controllers/ServiceGoalTest.js deleted file mode 100644 index 2ec830a..0000000 --- a/frontend/test/spec/controllers/ServiceGoalTest.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict'; - -var path = 'http://localhost:3000/'; - -describe('Service: ServiceGoal', function() { - var Goal, httpBackend; - - //load the service's module - beforeEach(module('ecoknowledgeApp')); - - //Initialize the controller and a mock Scope - beforeEach(inject(function (_$httpBackend_, _ServiceGoal_) { - httpBackend = _$httpBackend_; - Goal = _ServiceGoal_; - })); - - describe('Goal.get', function () { - var callbacks; - - beforeEach(function () { - callbacks = { - success: function () { - }, - error: function () { - } - }; - spyOn(callbacks, 'success').and.callThrough(); - spyOn(callbacks, 'error').and.callThrough(); - }); - - - it('should get some goals from the service', function () { - var mockClim = { - 'name': 'Clim', 'conditions': [ - {'required': 'Temperature', 'comparison': 'inf', 'value': 25}, - {'required': 'Temperature', 'comparison': 'eq', 'value': 18}, - {'required': 'Temperature', 'comparison': 'inf', 'value': 25} - ] - }; - var mockPatrick = { - 'name': 'Patrick', 'conditions': [ - {'required': 'Porte', 'comparison': 'inf', 'value': 'OPEN'}, - {'required': 'Temperature', 'comparison': 'eq', 'value': 18}, - {'required': 'Temperature', 'comparison': 'inf', 'value': 25} - ] - }; - var mockResult = mockClim, mockPatrick; - //backend definition returns a mock user - httpBackend.when('GET',path +'goals/all').respond(mockResult); - httpBackend.expectGET(path+'goals/all'); - Goal.get('', callbacks.success, callbacks.error); - - httpBackend.flush(); - - expect(callbacks.success).toHaveBeenCalled(); - expect(callbacks.error).not.toHaveBeenCalled(); - expect(callbacks.success.calls.argsFor(0)).toEqual([mockResult]); - }); - }); - - describe('Goal.post', function () { - var callbacks; - - beforeEach(function () { - callbacks = { - success: function () { - }, - error: function () { - } - }; - spyOn(callbacks, 'success').and.callThrough(); - spyOn(callbacks, 'error').and.callThrough(); - }); - - it('should send a new goal to the service', function(){ - var mockPatrick = { - 'name': 'Patrick', 'conditions': [ - {'required': 'Porte', 'comparison': 'inf', 'value': 'OPEN'}, - {'required': 'Temperature', 'comparison': 'eq', 'value': 18}, - {'required': 'Temperature', 'comparison': 'inf', 'value': 25} - ] - }; - - - httpBackend.when('POST',path +'goals/new'); - httpBackend.expectPOST(path+'goals/new').respond('gg wp'); - Goal.post(mockPatrick, callbacks.success, callbacks.error); - - httpBackend.flush(); - - expect(callbacks.success).toHaveBeenCalled(); - expect(callbacks.error).not.toHaveBeenCalled(); - }); - - }); -}); \ No newline at end of file diff --git a/frontend/test/spec/controllers/about.js b/frontend/test/spec/controllers/about.js deleted file mode 100644 index 6d127c4..0000000 --- a/frontend/test/spec/controllers/about.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -describe('Controller: AboutCtrl', function () { - - // load the controller's module - beforeEach(module('ecoknowledgeApp')); - - var AboutCtrl, - scope; - - // Initialize the controller and a mock scope - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new(); - AboutCtrl = $controller('AboutCtrl', { - $scope: scope - }); - })); - - it('should attach a list of awesomeThings to the scope', function () { - expect(scope.awesomeThings.length).toBe(3); - }); -}); diff --git a/frontend/test/spec/controllers/main.js b/frontend/test/spec/controllers/main.js deleted file mode 100644 index ace0540..0000000 --- a/frontend/test/spec/controllers/main.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -describe('Controller: MainCtrl', function () { - - // load the controller's module - beforeEach(module('ecoknowledgeApp')); - - var MainCtrl, - scope; - - // Initialize the controller and a mock scope - beforeEach(inject(function ($controller, $rootScope) { - scope = $rootScope.$new(); - MainCtrl = $controller('MainCtrl', { - $scope: scope - }); - })); - - it('should attach a list of awesomeThings to the scope', function () { - expect(scope.awesomeThings.length).toBe(3); - }); -}); From c428448deb734605f66c1491501168b3717c70c8 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 14:01:39 +0200 Subject: [PATCH 24/28] Delete .tsdrc file --- .tsdrc | 3 --- frontend/Gruntfile.js | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 .tsdrc diff --git a/.tsdrc b/.tsdrc deleted file mode 100644 index d8f386d..0000000 --- a/.tsdrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "token":"73d28fdc4f1e8b73f18ca6de7c64fd560e97d5fe" -} \ No newline at end of file diff --git a/frontend/Gruntfile.js b/frontend/Gruntfile.js index 8a0ebdf..128ba32 100644 --- a/frontend/Gruntfile.js +++ b/frontend/Gruntfile.js @@ -410,6 +410,9 @@ module.exports = function (grunt) { grunt.task.run(['serve:' + target]); }); + // Because npm test (which was not triggered) trigger this task + grunt.registerTask('test', []); + /* grunt.registerTask('test', [ 'clean:server', From 21a984a3bee42bdd6a3efbd7a8257d3f71ae9443 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 15:03:53 +0200 Subject: [PATCH 25/28] Fix peristence issues --- backend/db.json | 95 ++++++++++++------- backend/src/Context.ts | 12 ++- backend/src/JSONSerializer.ts | 2 +- backend/src/StoringHandler.ts | 2 + backend/src/api/DashboardRouter.ts | 9 +- backend/src/challenge/TeamChallengeFactory.ts | 19 +++- backend/src/challenge/UserChallenge.ts | 9 +- backend/src/condition/AverageOnValue.ts | 1 + backend/src/condition/ReferencePeriod.ts | 7 ++ backend/src/goal/Goal.ts | 8 +- .../views/homepage/homepage-challenge.html | 2 +- 11 files changed, 119 insertions(+), 47 deletions(-) diff --git a/backend/db.json b/backend/db.json index 87572ef..076becf 100644 --- a/backend/db.json +++ b/backend/db.json @@ -11,7 +11,7 @@ "conditions": [ { "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", - "description":"a desc", + "description": "a desc", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -21,14 +21,13 @@ "value": "15", "symbolicName": false }, - "comparison": ">", - "periodOfTime": "-2208474000000" + "comparison": ">" }, "threshold": 25, - "referencePeriod" :{ - "numberOfUnitToSubtract":1, - "unitToSubtract":"week" - }, + "referencePeriod": { + "numberOfUnitToSubtract": 1, + "unitToSubtract": "week" + }, "filter": { "dayOfWeekFilter": "all", "periodOfDayFilter": [ @@ -37,6 +36,29 @@ ] }, "type": "comparison" + }, + { + "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", + "description": "tmp_cli > 1", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": 28, + "symbolicName": false + }, + "comparison": ">" + }, + "threshold": 50, + "filter": { + "dayOfWeekFilter": "all", + "periodOfDayFilter": [ + "all" + ] + }, + "type": "overall" } ], "badgeID": "44bb8108-8830-4f43-abd1-3ef643303d92" @@ -52,7 +74,7 @@ "conditions": [ { "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", - "description": "tmp_cli > 1", + "description": "tmp_cli > 1", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -80,7 +102,7 @@ "badges": [ { "id": "44bb8108-8830-4f43-abd1-3ef643303d92", - "name": "Un challenge de démo !", + "name": "Un challenge de d\u00e9mo !", "points": 100 }, { @@ -88,7 +110,7 @@ "name": "Pas froid aux yeux", "points": "100" }, - { + { "id": "faa78334-f515-4563-954b-ac91b4a42f88", "name": "Pas chiche de tout fermer !", "points": "100" @@ -98,48 +120,55 @@ { "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", "name": "Charlie", + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_443V" + }, "currentChallenges": [ + ], "finishedBadgesMap": { "44bb8108-8830-4f43-abd1-3ef643303d92": 1, "fde68334-f515-4563-954b-ac91b4a42f88": 1 - }, - "mapSymbolicNameToSensor": { - "TMP_CLI":"TEMP_443V" - } + } }, - { + { "id": "6efce3142d44-a320-4766-4766-2cf91e02", - "name": "Gégé", + "name": "G\u00e9g\u00e9", + "mapSymbolicNameToSensor": { + "TMP_CLI": "AC_555V" + }, "currentChallenges": [ + ], "finishedBadgesMap": { "44bb8108-8830-4f43-abd1-3ef643303d92": 1, "fde68334-f515-4563-954b-ac91b4a42f88": 1 - }, - "mapSymbolicNameToSensor": { - "TMP_CLI":"AC_555V" - } + } } ], "teams": [ - { - "id" : "28aa8108-8830-4f43-abd1-3ab643303d92", - "name" : "croquette", - "members" : ["2cf91e02-a320-4766-aa9f-6efce3142d44", "6efce3142d44-a320-4766-4766-2cf91e02"], - "leader":"2cf91e02-a320-4766-aa9f-6efce3142d44", - "currentChallenges": [ + { + "id": "28aa8108-8830-4f43-abd1-3ab643303d92", + "name": "croquette", + "members": [ + "2cf91e02-a320-4766-aa9f-6efce3142d44", + "6efce3142d44-a320-4766-4766-2cf91e02" + ], + "leader": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "currentChallenges": [ + ], "finishedBadgesMap": { "44bb8108-8830-4f43-abd1-3ef643303d92": 2 } - } + } ], - "challenges": - { - "userChallenges": - [ - ], - "teamChallenges":[] + "challenges": { + "userChallenges": [ + + ], + "teamChallenges": [ + + ] } } \ No newline at end of file diff --git a/backend/src/Context.ts b/backend/src/Context.ts index 73475e2..1053898 100644 --- a/backend/src/Context.ts +++ b/backend/src/Context.ts @@ -145,13 +145,12 @@ class Context { var currentTeam = this._teamFactory.createTeam(currentTeamDescription, this._userRepository, this._teamChallengeFactory); this._teamRepository.addTeam(currentTeam); } - } fillChallengesRepository(data) { var challenges = data.challenges; this.fillUserChallengeRepository(challenges); - this.fillTeamChallengeRepository(data); + this.fillTeamChallengeRepository(challenges); } fillUserChallengeRepository(data) { @@ -166,9 +165,16 @@ class Context { } } - // TODO fillTeamChallengeRepository(data) { + var challenges = data.teamChallenges; + + for (var currentChallengeIndex = 0; currentChallengeIndex < challenges.length; currentChallengeIndex++) { + var currentChallengeDescription = challenges[currentChallengeIndex]; + + var currentChallenge = this._teamChallengeFactory.restoreTeamChallenge(currentChallengeDescription, this._teamRepository,this._goalRepository, this._userChallengeRepository, Clock.getMoment(Clock.getNow())); + this._teamChallengeRepository.addTeamChallenge(currentChallenge); + } } public getBadgeRepository():BadgeRepository { diff --git a/backend/src/JSONSerializer.ts b/backend/src/JSONSerializer.ts index 681e643..e378a1c 100644 --- a/backend/src/JSONSerializer.ts +++ b/backend/src/JSONSerializer.ts @@ -18,7 +18,7 @@ class JSONSerializer { return {success: '+++\tDatabase loaded correctly !\t+++', data: JSON.parse(data)}; } - public save(data:any, pathToFile:string, successCallBack:Function, failCallBack:Function):void { + public save(pathToFile:string,data:any, successCallBack:Function, failCallBack:Function):void { fs.writeFile(pathToFile, JSON.stringify(data, null, 2), function (err) { if (err) { diff --git a/backend/src/StoringHandler.ts b/backend/src/StoringHandler.ts index aa79b87..ab4403b 100644 --- a/backend/src/StoringHandler.ts +++ b/backend/src/StoringHandler.ts @@ -30,8 +30,10 @@ class StoringHandler { result['definitions'] = this.context.getGoalRepository().getDataInJSON(); result['badges'] = this.context.getBadgeRepository().getDataInJSON(); result['users'] = this.context.getUserRepository().getDataInJSON(); + result['teams'] = this.context.getTeamRepository().getDataInJSON(); result['challenges'] = {}; result['challenges']['userChallenges'] = this.context.getUserChallengeRepository().getDataInJSON(); + result['challenges']['teamChallenges'] = this.context.getTeamChallengeRepository().getDataInJSON(); this.serializer.save(pathToFile, result, successCallBack, failCallBack); } diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 0c5ae46..9fda428 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -589,8 +589,13 @@ class DashboardRouter extends RouterItf { if (challengeToEvaluate.getStatus() == ChallengeStatus.SUCCESS) { console.log("Le challenge est réussi et terminé"); - // Add finished badge to current user - this.addFinishedBadge(challengeID, entity.getUUID()); + + if(challengeToEvaluate.isAPersonalChallenge()) { + // Add finished badge to current user + this.addFinishedBadge(challengeID, entity.getUUID()); + } + entity.deleteChallenge(challengeToEvaluate.getID()); + // Build the new challenge (recurring) and evaluate it var newChallenge = self.createUserChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts index a258cbd..c9ffe0a 100644 --- a/backend/src/challenge/TeamChallengeFactory.ts +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -4,6 +4,8 @@ import Goal = require('../goal/Goal'); import UserChallenge = require('../challenge/UserChallenge'); import TeamChallenge = require('../challenge/TeamChallenge'); import UserChallengeRepository = require('../challenge/UserChallengeRepository'); +import TeamRepository = require('../user/TeamRepository'); +import GoalRepository = require('../goal/GoalRepository'); import uuid = require('node-uuid'); class TeamChallengeFactory { @@ -31,8 +33,23 @@ class TeamChallengeFactory { return new TeamChallenge(id, team, membersChallenges, userChallengeRepository); } - restoreTeamChallenge() { + restoreTeamChallenge(data, teamRepository:TeamRepository, goalRepository:GoalRepository, userChallengeRepository:UserChallengeRepository,now):TeamChallenge { + var id:string = data.id; + var teamID:string = data.team; + var team:Team = teamRepository.getTeam(teamID); + + var childrenIDs:string[] = data.children; + var children:UserChallenge[] = []; + + for(var currentChildIDIndex in childrenIDs) { + var currentChildID = childrenIDs[currentChildIDIndex]; + var currentChild = userChallengeRepository.getChallengeByID(currentChildID); + children.push(currentChild); + } + + + return new TeamChallenge(id, team, children, userChallengeRepository); } } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index e039d03..7ffc5e6 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -298,14 +298,19 @@ class UserChallenge { return result; } + isAPersonalChallenge():boolean { + return this.takenBy == this.getUser().getName(); + } getDataInJSON():any { return { id: this.id, startDate: this.startDate, endDate: this.endDate, - goal: this.goal.getUUID(), - user: this.user.getUUID(), + goalID: this.goal.getUUID(), + userID: this.user.getUUID(), + takenBy:this.takenBy, + mapConditionIDToSensorAndTimeBoxRequired: this.mapConditionIDToSensorAndTimeBoxRequired, progress: this.progress } } diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index 66754e1..7ae385c 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -121,6 +121,7 @@ class AverageOnValue extends Condition { public getDataInJSON():any { var data:any = super.getDataInJSON(); data.type = 'comparison'; + data.referencePeriod = this.referencePeriod.getDataInJSON(); return data; } diff --git a/backend/src/condition/ReferencePeriod.ts b/backend/src/condition/ReferencePeriod.ts index 6f1f5e1..30ffd6b 100644 --- a/backend/src/condition/ReferencePeriod.ts +++ b/backend/src/condition/ReferencePeriod.ts @@ -20,6 +20,13 @@ class ReferencePeriod { dateOfCreation = dateOfCreation.subtract(this.numberOfUnitToSubtract, this.unitToSubtract); return dateOfCreation; } + + getDataInJSON():any { + return { + numberOfUnitToSubtract: this.numberOfUnitToSubtract, + unitToSubtract:this.unitToSubtract + } + } } export = ReferencePeriod; \ No newline at end of file diff --git a/backend/src/goal/Goal.ts b/backend/src/goal/Goal.ts index 3a90151..aa1b8df 100644 --- a/backend/src/goal/Goal.ts +++ b/backend/src/goal/Goal.ts @@ -143,11 +143,11 @@ class Goal { return { id: this.id, name: this.name, - timeBox: { - startDate: this.beginningOfValidityPeriod, - endDate: this.endOfValidityPeriod + validityPeriod: { + start: this.beginningOfValidityPeriod, + end: this.endOfValidityPeriod }, - duration: this.recurringSession.getDescription(), + recurringPeriod: this.recurringSession.getDescription(), conditions: this.getDataOfConditionsInJSON(), badgeID: this.badgeID } diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index 9fa7ca6..3facad3 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -1,5 +1,5 @@
    - +
    From f00ae4c510ffaf81aef8fa241163a9d3c4eaccec Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Thu, 3 Sep 2015 16:25:28 +0200 Subject: [PATCH 26/28] Fix bug Team has a badge when a Team challenge is achieved Members don't. Just don't. Because. fix bug in percentageAchieved field in childs of teamChallenge fix bug in achieved in teamChallenge fix bug in dashboardRouter, used wrong repository --- backend/src/api/DashboardRouter.ts | 90 +++++++++---------- backend/src/challenge/TeamChallenge.ts | 23 ++++- .../src/challenge/TeamChallengeRepository.ts | 17 +++- backend/src/challenge/UserChallenge.ts | 1 - backend/src/condition/AverageOnValue.ts | 5 ++ backend/src/condition/OverallGoalCondition.ts | 6 +- backend/src/user/Team.ts | 12 +++ backend/tests/goal/GoalTest.ts | 8 +- 8 files changed, 100 insertions(+), 62 deletions(-) diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 9fda428..8ceb9c9 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -292,7 +292,7 @@ class DashboardRouter extends RouterItf { this.evaluateChallengeForGivenTeam(teamDescriptionWanted); // First col : available goal - var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.userChallengeRepository); + var descriptionOfAvailableGoals = this.goalRepository.getListOfNotTakenGoalInJSONFormat(teamDescriptionWanted, this.teamChallengeRepository); // Second col : badge description var descriptionOfBadges:any[] = this.buildBadgesDescriptionForGivenTeam(teamDescriptionWanted); @@ -441,48 +441,27 @@ class DashboardRouter extends RouterItf { var currentChallengeID = challenges[challengeIndex]; var currentChallenge = this.userChallengeRepository.getChallengeByID(currentChallengeID); - this.evaluateChallenge(user, currentChallenge, currentChallengeID); + if(currentChallenge.isAPersonalChallenge()) { + this.evaluateUserChallenge(user, currentChallenge, currentChallengeID); + } + else { + // Retrieve related TeamChallenge + var teamChallenge:TeamChallenge = this.teamChallengeRepository.getTeamChallengeFromUserChallengeID(currentChallengeID); + + // Retrieve team linked + var team:Team = teamChallenge.getTeam(); + + // Evaluate team challenge + this.evaluateTeamChallenge(team, teamChallenge, teamChallenge.getID()); + } } } - private evaluateTeamChallenge(entity, challengeToEvaluate:TeamChallenge, challengeID) { + private evaluateTeamChallenge(entity:Team, challengeToEvaluate:TeamChallenge, challengeID) { var self = this; if (!DashboardRouter.DEMO) { - //TODO move what follow - var required = challengeToEvaluate.getSensors(); - - var requiredSensorName = Object.keys(required); - var numberToLoad:number = requiredSensorName.length; - - for (var currentSensorName in requiredSensorName) { - (function (currentSensorName) { - var startDate:string = '' + required[currentSensorName].startDate; - var endDate:string = '' + required[currentSensorName].endDate; - - var path = 'http://smartcampus.unice.fr/sensors/' + currentSensorName + '/data?date=' + startDate + '/' + endDate; - var dataJsonString = ""; - - this.middleware.getSensorsInfo(required, numberToLoad, dataJsonString, path, - function () { - var result = challengeToEvaluate.evaluate(required); - if (result) { - var newChall = self.createUserChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); - this.addBadge(challengeID, entity.getUUID()); - if (newChall != null) { - self.evaluateChallenge(entity, newChall, newChall.getID()); - } - } - console.log("All data were retrieve properly"); - return challengeToEvaluate; - }, - function () { - return {error: "Error occurred in middleware"}; - }); - - })(requiredSensorName[currentSensorName]); - } } else { console.log('++++++++++++++++++++++ \tMODE DEMO\t+++++++++++++++++++++'); @@ -500,10 +479,14 @@ class DashboardRouter extends RouterItf { // Check if the challenge is achieved and finished if (result.achieved && result.finished) { - console.log("Le challenge est réussi et terminé"); + console.log("Le challenge de team est réussi et terminé"); - // Add finished badge to current user - this.addFinishedBadge(challengeID, entity.getUUID()); + var badgeID:string = challengeToEvaluate.getBadge(); + + // Add finished badge to current team + this.addFinishedBadgeToTeam(badgeID, entity); + + entity.deleteChallenge(challengeToEvaluate); /* // Build the new challenge (recurring) and evaluate it @@ -516,9 +499,9 @@ class DashboardRouter extends RouterItf { // Check if the challenge is not achieved but finished else if (!result.achieved && result.finished) { - console.log("Le challenge est FAIL et terminé"); + console.log("Le challenge de team est FAIL et terminé"); - entity.deleteChallenge(challengeToEvaluate.getID()); + entity.deleteChallenge(challengeToEvaluate); /* // Build the new challenge (recurring) and evaluate it @@ -534,7 +517,7 @@ class DashboardRouter extends RouterItf { } - private evaluateChallenge(entity, challengeToEvaluate:UserChallenge, challengeID) { + private evaluateUserChallenge(entity, challengeToEvaluate:UserChallenge, challengeID) { var self = this; if (!DashboardRouter.DEMO) { @@ -560,7 +543,7 @@ class DashboardRouter extends RouterItf { var newChall = self.createUserChallenge(entity, challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); this.addBadge(challengeID, entity.getUUID()); if (newChall != null) { - self.evaluateChallenge(entity, newChall, newChall.getID()); + self.evaluateUserChallenge(entity, newChall, newChall.getID()); } } console.log("All data were retrieve properly"); @@ -589,18 +572,16 @@ class DashboardRouter extends RouterItf { if (challengeToEvaluate.getStatus() == ChallengeStatus.SUCCESS) { console.log("Le challenge est réussi et terminé"); + // Add finished badge to current user + this.addFinishedBadgeToUser(challengeID, entity.getUUID()); - if(challengeToEvaluate.isAPersonalChallenge()) { - // Add finished badge to current user - this.addFinishedBadge(challengeID, entity.getUUID()); - } entity.deleteChallenge(challengeToEvaluate.getID()); // Build the new challenge (recurring) and evaluate it var newChallenge = self.createUserChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(entity, newChallenge, newChallenge.getID()); + self.evaluateUserChallenge(entity, newChallenge, newChallenge.getID()); } } @@ -613,7 +594,7 @@ class DashboardRouter extends RouterItf { // Build the new challenge (recurring) and evaluate it var newChallenge = self.createUserChallenge(entity.getUUID(), challengeToEvaluate.getGoal().getUUID(), challengeToEvaluate.getEndDate()); if (newChallenge != null) { - self.evaluateChallenge(entity, newChallenge, newChallenge.getID()); + self.evaluateUserChallenge(entity, newChallenge, newChallenge.getID()); } } @@ -622,7 +603,7 @@ class DashboardRouter extends RouterItf { } // debug only - private addFinishedBadge(challengeID:string, userID:string) { + private addFinishedBadgeToUser(challengeID:string, userID:string) { /* console.log('add finished badge'); console.log('user id : ', userID); @@ -634,6 +615,15 @@ class DashboardRouter extends RouterItf { user.deleteChallenge(challengeID); } + private addFinishedBadgeToTeam(badgeID:string, team:Team) { + /* + console.log('add finished badge'); + console.log('user id : ', userID); + console.log('challenge ID : ', challengeID); + */ + team.addBadge(badgeID); + } + createUserChallenge(userID:string, goalID:string, date:moment.Moment):UserChallenge { diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index 9187dc4..9b284f8 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -44,6 +44,17 @@ class TeamChallenge { this.checkChildrenTimeBoxes(); } + getChildUserChallengeByID(userChallengeID:string):UserChallenge { + for(var currentChildIndex in this.childChallenges) { + var currentChild = this.childChallenges[currentChildIndex]; + if(currentChild.getID() == userChallengeID) { + return currentChild; + } + } + + return null; + } + getID():string { return this.id; } @@ -56,6 +67,10 @@ class TeamChallenge { return this.childChallenges[0].getName(); } + getTeam():Team { + return this.team; + } + getBadge() { return this.childChallenges[0].getBadge(); } @@ -146,7 +161,8 @@ class TeamChallenge { var childResult = currentChild.evaluate(data); - achieved = achieved && currentChild.getStatus() == Status.SUCCESS; + console.log("SON RESULT\n", childResult, '\n\n'); + achieved = achieved && childResult.achieved; var currentChildGlobalProgression:number = childResult.percentageAchieved; childProgress += currentChildGlobalProgression; @@ -161,10 +177,11 @@ class TeamChallenge { } this.durationAchieved = this.childChallenges[0].getTimeProgress(); - this.progress['percentageAchieved'] = childProgress / this.childChallenges.length; + var percentageAchieved = childProgress / this.childChallenges.length; + this.progress['percentageAchieved'] = percentageAchieved; this.progress['durationAchieved'] = this.durationAchieved; this.progress['finished'] = this.durationAchieved == 100; - this.progress['achieved'] = achieved; + this.progress['achieved'] = achieved || percentageAchieved == 100; this.progress['conditions'] = childProgressDescription; return this.progress; diff --git a/backend/src/challenge/TeamChallengeRepository.ts b/backend/src/challenge/TeamChallengeRepository.ts index fd76a71..963755a 100644 --- a/backend/src/challenge/TeamChallengeRepository.ts +++ b/backend/src/challenge/TeamChallengeRepository.ts @@ -22,9 +22,20 @@ class TeamChallengeRepository { getChallengeByID(challengeID:string):TeamChallenge { for (var i in this.teamChallengesArray) { - var currentBadge = this.teamChallengesArray[i]; - if (currentBadge.hasUUID(challengeID)) { - return currentBadge; + var currentChallenge = this.teamChallengesArray[i]; + if (currentChallenge.hasUUID(challengeID)) { + return currentChallenge; + } + } + + return null; + } + + getTeamChallengeFromUserChallengeID(userChallengeID:string):TeamChallenge { + for (var i in this.teamChallengesArray) { + var currentChallenge = this.teamChallengesArray[i]; + if (currentChallenge.getChildUserChallengeByID(userChallengeID) != null) { + return currentChallenge; } } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 7ffc5e6..28cc090 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -263,7 +263,6 @@ class UserChallenge { if (achieved && finished) { this.status = BadgeStatus.SUCCESS; console.log('success!'); - return true; } else if (!achieved && finished) { this.status = BadgeStatus.FAIL; console.log('Fail!'); diff --git a/backend/src/condition/AverageOnValue.ts b/backend/src/condition/AverageOnValue.ts index 7ae385c..c212a16 100644 --- a/backend/src/condition/AverageOnValue.ts +++ b/backend/src/condition/AverageOnValue.ts @@ -82,6 +82,11 @@ class AverageOnValue extends Condition { // It can be infinite percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; + // If there is no values yet + if(isNaN(percentageAchieved)) { + percentageAchieved = 0; + } + } var achieved:boolean = percentageAchieved === 100; var result:any = {description: this.description, percentageAchieved: percentageAchieved, achieved: achieved}; diff --git a/backend/src/condition/OverallGoalCondition.ts b/backend/src/condition/OverallGoalCondition.ts index d1410ec..79cba45 100644 --- a/backend/src/condition/OverallGoalCondition.ts +++ b/backend/src/condition/OverallGoalCondition.ts @@ -51,7 +51,6 @@ class OverallGoalCondition extends Condition { // Check value by value if internal condition is satisfied if (this.expression.evaluate(dataToEvaluate)) { - console.log("OK"); ++numberOfCorrectValues; } @@ -61,6 +60,11 @@ class OverallGoalCondition extends Condition { var percentageAchieved = ((numberOfCorrectValues * 100 / numberOfValues) * 100) / this.thresholdRate; percentageAchieved = (percentageAchieved > 100) ? 100 : percentageAchieved; + // If there is no values yet + if(isNaN(percentageAchieved)) { + percentageAchieved = 0; + } + var achieved:boolean = percentageAchieved === 100; return {description: this.description, percentageAchieved: percentageAchieved, achieved: achieved}; diff --git a/backend/src/user/Team.ts b/backend/src/user/Team.ts index 9de24fe..f478236 100644 --- a/backend/src/user/Team.ts +++ b/backend/src/user/Team.ts @@ -79,6 +79,18 @@ class Team { return newChallenge; } + addBadge(badgeID:string) { + if (!badgeID) { + throw new BadArgumentException('Can not add given badge to team' + this.getName() + '. Badge given is null'); + } + + if (this.badgesMap.hasOwnProperty(badgeID)) { + this.badgesMap[badgeID]++; + } else { + this.badgesMap[badgeID] = 1; + } + } + deleteChallenge(challenge:TeamChallenge):void { var challengeIndex:number = this.getChallengeByID(challenge.getID()); diff --git a/backend/tests/goal/GoalTest.ts b/backend/tests/goal/GoalTest.ts index ecdc73c..33425a1 100644 --- a/backend/tests/goal/GoalTest.ts +++ b/backend/tests/goal/GoalTest.ts @@ -93,11 +93,11 @@ describe('Goal Test', () => { var expected:any = { id: goal.getUUID(), name: goal.getName(), - timeBox: { - startDate: goal.getBeginningOfValidityPeriod(), - endDate: goal.getEndOfValidityPeriod() + validityPeriod: { + start: goal.getBeginningOfValidityPeriod(), + end: goal.getEndOfValidityPeriod() }, - duration: aRecurringSession.getDescription(), + recurringPeriod: aRecurringSession.getDescription(), conditions: goal.getDataOfConditionsInJSON(), badgeID: goal.getBadgeID() }; From a1d230a629d2acbffc8495171ca7fc9a63469738 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Fri, 4 Sep 2015 10:14:43 +0200 Subject: [PATCH 27/28] Prettify for demo --- backend/db.json | 51 +------ backend/new-dv-save.txt | 133 ++++++++++++++++++ backend/src/api/DashboardRouter.ts | 17 ++- backend/src/challenge/TeamChallenge.ts | 33 ++++- backend/src/challenge/UserChallenge.ts | 2 +- frontend/app/images/Thumbs.db | Bin 0 -> 119296 bytes frontend/app/images/down.png | Bin 0 -> 5320 bytes frontend/app/images/fail.jpeg | Bin 0 -> 13819 bytes frontend/app/images/fail.png | Bin 0 -> 42220 bytes frontend/app/images/shame.jpg | Bin 0 -> 65156 bytes frontend/app/images/youfail.png | Bin 0 -> 8773 bytes .../scripts/controllers/ServiceDashboard.js | 3 +- frontend/app/scripts/controllers/dashboard.js | 8 +- frontend/app/styles/homepage-challenges.css | 16 ++- frontend/app/views/dashboard.html | 1 + .../views/homepage/homepage-challenge.html | 30 +++- 16 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 backend/new-dv-save.txt create mode 100644 frontend/app/images/Thumbs.db create mode 100644 frontend/app/images/down.png create mode 100644 frontend/app/images/fail.jpeg create mode 100644 frontend/app/images/fail.png create mode 100644 frontend/app/images/shame.jpg create mode 100644 frontend/app/images/youfail.png diff --git a/backend/db.json b/backend/db.json index 076becf..fcf9cd3 100644 --- a/backend/db.json +++ b/backend/db.json @@ -11,7 +11,7 @@ "conditions": [ { "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", - "description": "a desc", + "description": "la température a augmenté de 25% par rapport à la semaine dernière", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -39,7 +39,7 @@ }, { "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", - "description": "tmp_cli > 1", + "description": "tmp_cli > 28, 50% du temps", "expression": { "valueLeft": { "value": "TMP_CLI", @@ -62,41 +62,6 @@ } ], "badgeID": "44bb8108-8830-4f43-abd1-3ef643303d92" - }, - { - "id": "9bddaf87-5065-4df7-920a-d1d249c9171d", - "name": "Obj1", - "validityPeriod": { - "start": "2015-08-04T12:25:57.787Z", - "end": "2015-08-31T12:25:57.787Z" - }, - "recurringPeriod": "week", - "conditions": [ - { - "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", - "description": "tmp_cli > 1", - "expression": { - "valueLeft": { - "value": "TMP_CLI", - "symbolicName": true - }, - "valueRight": { - "value": 1, - "symbolicName": false - }, - "comparison": ">" - }, - "threshold": 100, - "filter": { - "dayOfWeekFilter": "working-week", - "periodOfDayFilter": [ - "all" - ] - }, - "type": "overall" - } - ], - "badgeID": "fde68334-f515-4563-954b-ac91b4a42f88" } ], "badges": [ @@ -108,12 +73,7 @@ { "id": "fde68334-f515-4563-954b-ac91b4a42f88", "name": "Pas froid aux yeux", - "points": "100" - }, - { - "id": "faa78334-f515-4563-954b-ac91b4a42f88", - "name": "Pas chiche de tout fermer !", - "points": "100" + "points": "40" } ], "users": [ @@ -127,9 +87,8 @@ ], "finishedBadgesMap": { - "44bb8108-8830-4f43-abd1-3ef643303d92": 1, - "fde68334-f515-4563-954b-ac91b4a42f88": 1 - } + "44bb8108-8830-4f43-abd1-3ef643303d92": 1 + } }, { "id": "6efce3142d44-a320-4766-4766-2cf91e02", diff --git a/backend/new-dv-save.txt b/backend/new-dv-save.txt new file mode 100644 index 0000000..fcf9cd3 --- /dev/null +++ b/backend/new-dv-save.txt @@ -0,0 +1,133 @@ +{ + "definitions": [ + { + "id": "3221c575-85ca-447b-86f3-3a4ef39985dc", + "name": "Clim", + "validityPeriod": { + "start": "2015-07-01T06:58:42.000Z", + "end": "2015-09-30T13:01:24.000Z" + }, + "recurringPeriod": "week", + "conditions": [ + { + "id": "ab72f9b4-a368-4ea2-8adb-738ea0e6f30b", + "description": "la température a augmenté de 25% par rapport à la semaine dernière", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": "15", + "symbolicName": false + }, + "comparison": ">" + }, + "threshold": 25, + "referencePeriod": { + "numberOfUnitToSubtract": 1, + "unitToSubtract": "week" + }, + "filter": { + "dayOfWeekFilter": "all", + "periodOfDayFilter": [ + "morning", + "afternoon" + ] + }, + "type": "comparison" + }, + { + "id": "7713cb13-e86d-40d0-a39f-c4ad5a33546d", + "description": "tmp_cli > 28, 50% du temps", + "expression": { + "valueLeft": { + "value": "TMP_CLI", + "symbolicName": true + }, + "valueRight": { + "value": 28, + "symbolicName": false + }, + "comparison": ">" + }, + "threshold": 50, + "filter": { + "dayOfWeekFilter": "all", + "periodOfDayFilter": [ + "all" + ] + }, + "type": "overall" + } + ], + "badgeID": "44bb8108-8830-4f43-abd1-3ef643303d92" + } + ], + "badges": [ + { + "id": "44bb8108-8830-4f43-abd1-3ef643303d92", + "name": "Un challenge de d\u00e9mo !", + "points": 100 + }, + { + "id": "fde68334-f515-4563-954b-ac91b4a42f88", + "name": "Pas froid aux yeux", + "points": "40" + } + ], + "users": [ + { + "id": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "name": "Charlie", + "mapSymbolicNameToSensor": { + "TMP_CLI": "TEMP_443V" + }, + "currentChallenges": [ + + ], + "finishedBadgesMap": { + "44bb8108-8830-4f43-abd1-3ef643303d92": 1 + } + }, + { + "id": "6efce3142d44-a320-4766-4766-2cf91e02", + "name": "G\u00e9g\u00e9", + "mapSymbolicNameToSensor": { + "TMP_CLI": "AC_555V" + }, + "currentChallenges": [ + + ], + "finishedBadgesMap": { + "44bb8108-8830-4f43-abd1-3ef643303d92": 1, + "fde68334-f515-4563-954b-ac91b4a42f88": 1 + } + } + ], + "teams": [ + { + "id": "28aa8108-8830-4f43-abd1-3ab643303d92", + "name": "croquette", + "members": [ + "2cf91e02-a320-4766-aa9f-6efce3142d44", + "6efce3142d44-a320-4766-4766-2cf91e02" + ], + "leader": "2cf91e02-a320-4766-aa9f-6efce3142d44", + "currentChallenges": [ + + ], + "finishedBadgesMap": { + "44bb8108-8830-4f43-abd1-3ef643303d92": 2 + } + } + ], + "challenges": { + "userChallenges": [ + + ], + "teamChallenges": [ + + ] + } +} \ No newline at end of file diff --git a/backend/src/api/DashboardRouter.ts b/backend/src/api/DashboardRouter.ts index 8ceb9c9..6a08e20 100644 --- a/backend/src/api/DashboardRouter.ts +++ b/backend/src/api/DashboardRouter.ts @@ -113,7 +113,10 @@ class DashboardRouter extends RouterItf { teamDescription.name = currentTeam.getName(); dashboardList.push(teamDescription); } - + result.user = { + username : currentUser.getName(), + dashboard:'Personnel' + }; result.dashboardList = dashboardList; } @@ -152,6 +155,10 @@ class DashboardRouter extends RouterItf { dashboardList.push(teamDescription); } + result.user = { + username : currentUser.getName(), + dashboard:team.getName() }; + result.dashboardList = dashboardList; } @@ -471,6 +478,11 @@ class DashboardRouter extends RouterItf { challengeToEvaluate.setStatus(ChallengeStatus.RUN); } + if(challengeToEvaluate.getStatus() == ChallengeStatus.FAIL) { + console.log("Le challenge est fail, ça sert à rien de l'évaluer"); + return challengeToEvaluate; + } + var result:any = challengeToEvaluate.evaluate(this.jsonStub); @@ -501,7 +513,7 @@ class DashboardRouter extends RouterItf { else if (!result.achieved && result.finished) { console.log("Le challenge de team est FAIL et terminé"); - entity.deleteChallenge(challengeToEvaluate); + //entity.deleteChallenge(challengeToEvaluate); /* // Build the new challenge (recurring) and evaluate it @@ -612,7 +624,6 @@ class DashboardRouter extends RouterItf { var user = this.userRepository.getUser(userID); var badgeID = this.userChallengeRepository.getBadgeByChallengeID(challengeID); user.addBadge(badgeID); - user.deleteChallenge(challengeID); } private addFinishedBadgeToTeam(badgeID:string, team:Team) { diff --git a/backend/src/challenge/TeamChallenge.ts b/backend/src/challenge/TeamChallenge.ts index 9b284f8..e661e36 100644 --- a/backend/src/challenge/TeamChallenge.ts +++ b/backend/src/challenge/TeamChallenge.ts @@ -103,14 +103,18 @@ class TeamChallenge { } getStatus():Status { - var succeed:boolean = false; - var stillRunning:boolean = false; + var succeed:boolean = true; + var stillRunning:boolean = true; for (var currentChildIndex in this.childChallenges) { var currentChild = this.childChallenges[currentChildIndex]; var currentChildStatus = currentChild.getStatus(); - console.log("Current child status", currentChildStatus); + console.log("Current child status", currentChild.getStatusAsString()); + + if(currentChildStatus == Status.WAIT) { + return Status.WAIT; + } stillRunning = stillRunning && (currentChildStatus == Status.RUN); if (stillRunning) { @@ -183,6 +187,7 @@ class TeamChallenge { this.progress['finished'] = this.durationAchieved == 100; this.progress['achieved'] = achieved || percentageAchieved == 100; this.progress['conditions'] = childProgressDescription; + this.progress['status'] = this.getStatusAsString(); return this.progress; } @@ -208,6 +213,26 @@ class TeamChallenge { return result; } + getStatusAsString():string { + switch (this.getStatus()) { + case 0: + return 'WAIT'; + break; + case 1: + return 'RUNNING'; + break; + case 2: + return 'SUCCESS'; + break; + case 3: + return 'FAIL'; + break; + default: + return 'UNKNOWN'; + break; + } + } + getDataInJSON():any { var childrenIDs:string[] = []; for (var currentChildrenIndex in this.childChallenges) { @@ -235,7 +260,7 @@ class TeamChallenge { goal: this.childChallenges[0].getGoal().getName(), user: this.getName(), progress: this.progress, - status: this.status + status: this.getStatusAsString() } } } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 28cc090..611af8e 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -332,7 +332,7 @@ class UserChallenge { } - private getStatusAsString():string { + getStatusAsString():string { switch (this.status) { case 0: return 'WAIT'; diff --git a/frontend/app/images/Thumbs.db b/frontend/app/images/Thumbs.db new file mode 100644 index 0000000000000000000000000000000000000000..84bf64dbc2d0a8f03bea216021c40e6c186e7fb3 GIT binary patch literal 119296 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

    DdB#aWtF)abxB4{cKA?n39X4h$~F9Wgrv!< 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 literal 0 HcmV?d00001 diff --git a/frontend/app/images/fail.png b/frontend/app/images/fail.png new file mode 100644 index 0000000000000000000000000000000000000000..21164aa7e50a0acf544cb94331fcc211107a2dfc GIT binary patch literal 42220 zcmc$FWm{Wa({?EocP9jQ*FuUGhvF{9U4mQi;ts{3IKf(=5VVwF!HT=POQA^7LgAs; zeH`y!ct0fBdws~BHFM6)IWud=YN#pTU;;2-ym*15q$sQP;suiF^Yw3Z)aUQ9wcvv1 zA0$sL1?d-G$AE{=Hz;;es!}grz>={ZEM7g|W4I|AdcJr;==JY|v}Q{Kd0r&GP^<3%gT&C4zHkYKg`;zg;6lB|>t#Ps;DZ=msf1{0>fT8qcn ztD=umyb79=u(k@jKR^8hT&Hpm3vj0$R#tt6eU|IF-lt}Gk}~Zv?&W!^y+~1Hq$gtR z0?NJ`_NjF}^)#f%Fi~Pe!hhlYWYGCV#JkEx>}PfRxI}={i%N}R>Bu-y^gekKcAZL% z$q)^&&hrW;$zQbpUrXQ8AXN|)>SVjk!~p7|MHjCp7BjrEDP7t)B;F3jS?R@Q;ntyz&HW07#Sn!C<*Dbj)H9GHFHt3F!nK6ngCt^ zuY*QW4LeVZ8kV;q74YP?S8<8|^z6=IAY4x_s^t!5ektzQw2ACWP9@7zc+h?@m#tAD zuu-`j_NOl6hiibG=RdB^i8bsywv$^bl5->9=si^)CPorzHv zc;;=dqyBzH8A3Qz`=Y>lfAzJIRxCU=ZMH&!bue9%-kF;w;c@vn%nTP@vOB6*}CDHdtU%hfG6f}DOgUgAF6EIcRl_gjk#0+#g7n4@9v zfV0&8__7oW18qU-T8I@yyi+l-BEl?UWq~{fx=PSZQBtIiEerR@F=E8|bRrLIr0BM= zY7R|j3#?1YeJ?xu1!hE1Ywo4U&oFw|PG829P ze2ugV9oTlR#dF=?*~p4~btydb^jCL5_YyzJpx)hxVKYuGUK3B%cz-mK!)Yic~b z$kR0?q5q(_(r%zSlxgPcuu?g-IB{~d9U~v_&wtk@jU@Q}1Z4i&yhby;{nesDMj?U- zx8YXRY$FTWZ}mk0%jO!K$NSw)$*$M19hC)RKRXNU z#l$=drYBQQFcn0JtBX%RCh~OV~)mEeqbI^Yvn6rjElA+{A+wC z84zx_$_K{UDeBwoc@s`S)@ZbODz4NUFjX>ukw&4wh&QB=2p!FMvSO9_53Vva9Q`U? z?;lxhQHczIBmEw1VGNT5_P?`Aq=<(%sU>HWzHt2s(FuLi+0w+sjT zi*8C#A}scW-LA(qPk$nj5$>6?xHD5mxm=E?0gcH2z#E0T_5kI(d^w8XW6GwCwG5(9 zWYXgiLP<3s8AFUs@hun#YJnR#Q{yAY$mi~-vR$sWufmMQ@&tH0W_*<*Y2uhB9K7+* z0cR=l%18R)0ujRi@swI31r-U-^4|w{%f-0=*#g^zbBrc3vegf=SXGhi@qT$B<$#^D zZ!s*T(k%iu(8HLQs4wQh>5M7~G0(nz;CXtKIv3H%=C|d%w%#x5`-OOwsEL2-Cr(hS z7S3_`h+KorqGD0qtni;bzWmP~8$ItogGbIIVRYm@KI-H@!9HNMNKFZ?ByM|3p_k5w zmO8iEtjQ$i&YX!iKk~*Og6lS$(5Sn< z{p#e404) z+FIW-JeXQ=lirqZeFuMzlLs6l{%3U6JXDD|hJFU0xO)+s7bpu?*;E{em#%;+_?rb% zwytXkwNSs7L2iE676DV?a&YBl2>z=Y9!}ZSy2%B{x)KC^aUXke&eAx+dBLn=Uyunmp&=gd}^`fW&IeBhreSbmO=nsyOX`(@os`-Tsmz!mMHW^(i^eLkH!YhycV8nq~-F`*&_ACwW@hHq2LD!USs>2o$%VKlvpb~j%DfxRQ@46P6rEo5l1 z30cavoQjNm6<%%%G%IYJR7#}XG*6Ns?l*!fhp(A&+JG3{(s3rf^(KLIz3L;3)s#Mb+g+TPQnym$yf2$n(c&vx!f3IyPC`LKPuX^z@W*J? za&#dBB7@D*K6swi2u%o&Gw!H*7y;av7uXrcd=4qdqfqJFhlyn*w;N%J2#OfjAG%F) zR;?v#PM=as7Mm9-dUgg;cXRs1MKB5@R4BU1ms48IafX(=Bgp-obdm zx=r*#=wE_5_wG9;xG{IBCJ%^XDY5-lZQvA@H~=s$p;Webf6fc1iQ6*Cq8!#z0;@#8 z=1vj69t3?7IQs%Vlbce$x!iU-6?~kf=?$GEa-+|Q7E08^cP1+CdG}?$e<PBL)8CUlWa`|BI%?*8IX4sJ@Iztayc6d#|UR>6ab63EfI}kS0?6oDVzxe#Qlp` zTR+1NdtCE2*c*B^%g>8eKP57=QF6tm7}zCSDd`cIjsX5p1@ms#R09Rm6UwA`5&$LT zYV@l@$SK?erx;Z59tE(_)lCw6$PoU2dcTyatU0iN6u!Xg>Dd;Mal-4B!v zjIvwhcI>$R8xSdH0N}xgwY=Zz;pNFl_I($%ZTG6@{A`|;=9z*J#d3NDZYR65r-u^( zsKzLo=reD`HnrnJiR+RS4a147sJUo6k8oCoc76p@V2W!=}TyX4YP$zr_TIE=}KW9(BQ0aF6A zomwZqP`vxk26YxW)5*V5Zi-fpWRw}va)0csHR`@N`Gk~ic*afCX=`Qqql#na9c`AMYW#e2(0 z!5@Fz^e)ircbR(at^k4Ne3+81E&UPDOYn#<-WvOm8S1}8@rMY@&8UuS4t)z^zHC<8emVVK+T*?B9_>M~?|hhe38z8!~OLXi_jA6`lIrp)>-5G6HYD z|I4*Ok!5l{bt1Jd8sPI66yURdO$zq68N~g3X|GO%pfRJ>uw3`9ldi8CFPwVlMzA8z zZ8v^Z@rq`j{ZvHv3_}9b4?|bTK@&a?_WKsXv?dbVDw!pCr9k&9rdn;Qe!#I?`zRW76x~?o(O?@R;9A%De)SXA1sP9MRgE;?%T=2p6|wo@N{mR z1mnZo#7&>a2?K`;Atn&+VmFv$rb~C zFmbtu?%FXB$WE2yEn6fC_L)HqtMmkE*a;Rdg5`hCMr^NM`uAJQRjfvORN-(q>v8^| zyp2gw_?7>mMayuZFfATRiYDUapT`G~OGmC+bE%@bcmA@a>rS?9<4QW0{1>&oeeN23 z^t;UYal~l=uYvy7Tk7RBjfM&O-^4e?#CnEG#qWZvC*CcXGp08A*X0v~Wq6Zh0Lb4T zH1ph1W2~=0wJjSm;W%W6f1^c+Bp5RSOzpZb_;>N%j~MLNTcHt{j{n6b4hCRZ*ocKf z)o7A_a)OFyH%s-x1#FR*jtQ?tb)Kck+9+fu-nN6faGD8>7b5OMXKCN+61hiK6}D)rfAHz z-=!*7Nc-``JjxOpSBtl|;>kk|OsIy!n`xW42*myw8f8_BI!8D^iFJU2EGF#fs_oqPlurU|g z!|TIhgx;SO)r|>G=1m_Zgl!dpr&hSSR0n1JR?$umTLW2s>DV|3Tv=o#u~l!0ES@Xg zLh~={y8CX=>fn~|?`*5;zfj)3TQGoF-Q{Ah#mmE3TkwB3c%G>-Cir>~%{<(lJf~ag zLNB}mnm3q|zGjp_U)8(>l0oBRY=wz;JN|JoP0K|Dahctv{t*)n(H3i;tX&WaADuw%0Fe=wBzR(J)BtM#N)7YyXAQQI-3Kb^ED{}Hk`}Mv_YzX&RF!$4)?$+;7JO*&h(A+# zS77evcf4_a7nIK%w1P5%#{v_`?Dt5-^`7L^f&g>^;FZtaq_X*KP-q6nJmWSHRm^Zs z&3;?ICP9dPPbgon$Rq%V%byFt1l34B(PP0i%rhWQp8A@RY)1=|BI{SO53t#6IyiNVvZ?l6>?9;`%S?-&%egW@T# zVgq$1opO7k1wub+ROh22bBmfBO!1!|#Xxo=Hgk5HR63|;=7{p=Lt|S888-2q1D3XA z+GnHtI=c0TkNI42aaou@dfaT+^F`oYG8?<$9}=F&Rbp%X9x!$R$A<*tTGst1cHfp8 zIpvAk@wB1)jCqg5_r6hXnonVLVA`t&@tyuLILIfx{5&Euq~t-8{SU5y7*ci1ou(X$ zM;CcFpk8ne`~D)2C`2-obo@O4S^TV2pr%nY1dx%FB!=#~D;MxxK@M z?~1Adm%jU?S_yTQ6!f1gb+^R$yjC#4KXDr?i8d_$%HC^#+OIX@QzxfGWL3l}JDHe| zpaCP;9Zy1dkWA{IlDm3dv5CDFLTl*cd)a`w8~at;N*=1^)X$_IAyPz=Z(E^(Y$HV@ zv>(C8aWZfd>sClj)x$&cjn6}>ug2n!@UI@kx$t+3OtPeYQF%PuTrq>6m=;8a6uH?z zXQ@Eiz8F?ydwzwwS+bslk}1mIdmBh7x`(q^99L?Dd{)+vY1)`;;&zW15+c_qFV{@S zT~mJ7^3xhQEn$OsQ?MIzehw9zw05_+J@20b$Q8(SW&@ZNG@oYFlIw8BYn~i*AUpe9 zypXRL2!gRqr$p)m+g{{?^sWb^HGezvKQoLHu^lq0vw@Bhkr z9M~Sys2G5n@lR#*4Du-l?!8Qe-=CB=!wu}*&D$npp}$y{kd+$0hl4hMZ%0`7kU&-O zfmKav`Y+xyAi*=4@G^zN3vCC^Of|Wi_bOloKQ6*J!dwywOpK6|h|V!zDL>^SopNkz zeWMn~_%>``{>1k}UUKZ?)@luG`d50g1?{<(9)Vt@ho%GGR%19CHI9V))E&`XJ>LDd z?zngHWVH?||1}Sp|7|ZY`?U+G!25?|DR?JnVP{n4b^BI@B&ul;CfCzpa-A4KuXI*K z%^?`B*5c!5O1d^ootMTW^D0@DvyTA-&r}_)&Ow^bhq$6{%DLAl7Zt5?o93oEyNv?@Lz8$Dk6)KNkA-y*8K0^iegdkDZ=QVq#*W=w_4m^BPrb;8OktA2#tL z3y!1ma}TqjCz*MWOL_w$4GUMyGQrgcO3X|bwM+^N7Wh6J3psf{oIoYTIGf=OY~iHm zlj6%$8BpAa7D0rE1)QLuFeNlWZfo?T(f6AcQqW&IMA&yuc<{W^jA|bX^tNh~HLCM)Yrqg=}d&A z9<7$9)VZu|RBN`hS%Z@N!@q%jWBK1w`HPsTeRQecjwq;3v@2{pl_8={)X&z~oEWkF_N z^9>4uhJ;dgh2kNN7!i=xDd)O7KHLxoTcQ|H3&4)f$4GfmDprtOrsns067lmW{__D1 zCr}jN36}tS_-pQ7Lgnq4%Z6eNRY2;!Ua=xVJPkI+j9&PQR16ni1RN$6Q5KYz^QI2$ z-!t~?!ecN6{j0G=9xK@aQNc6STq z3PXa1+6z*5*K1F5-kzro384+kfw}~5dC)$Wq$Q{4OTNGQtPW8MFi`lIhzGuhr7TDU z5@y=sKx>QNfVL#y?^>Xo@#!i)7@2^R1rreVxD!scfR;&W01K};MK`Kx!4X55GUK9U z>IA!VlUJ$b)eP)P7A|5;7byEpmaz|I&U~h1&bmk+IptF@vQhQ|y}5MjKV!36YU!DRBeDQQp$+dO z%3vGmEU{D_ptZ4~ViUI05Zd#vlL=5gNr`Kg2@8IV$S2BhUF--lY(m`SyzH#+cd^5v zAD;O^M^8B#^M(mOPsL5=9g6a-*zwKsV;|r-}zF?-L+~Nc^$<-|g4UWZ`fw_3CpJ-4-dHz*&|iWk5Akrt9~-RzsgwIFjkFN)@Ru$i7Gn6!hdkTI6DP1;7zE5T23 z&WFQz{L=EOBffVyRWsZ-RrNXfpKSicxJcaFVM0?J^?(e&GJDD591LvmheeC*ON5{= z6Y`DtC}nG>_V=`gFEf6Be?AXo*~sb{u+*c+nk|F6u__`%cbH}!d(hT+2?#zL)0YV` zqxHv)?Op(R?EK%}ex$PQ{JQWH)8z)!L5~1ID`v z@^{5O$N2jJDZi!_taVK{Z)6d(f^M8K*lS0`|LjSvF3C$Rz`S}8zN$XdG_tAC&Xa_U z%Hm@vwm=wVIhJva^~b<++HAcra%?BnYoug?0(E9yhJyjiHlj8vY9OD;gepql zk|N5N*?6}yTly9GTvTtBu9;TK&o%jqIb=L}bYZqM+-*R}YYHOnHU^X&hh0t!D(c8FG*pl3XjDXy@a_!dy}EwSRc}ChZm48og>!bG;iCSuQ&s4i z=rQP+ITx2Y7!v8H-tW5yZuID)S3oOQ8-;e_!P3an@(w)J{mYK{0vp0dxgq**2UCNW57^d}|ik!2EJOYAsgj7Kw+ z`b0kET=SrQsHz2;jSP6i{mAG>S&`ajV4m%j%?o_UOU_Cwr)fEqi+J0zChcg5AP7(# zk%Ei(yQ$WB&=G>TzbP28fR}-Y3Q(k`)zgL*bF1hS<%>K5%j* z77}0PpQ`nBnRh_Zql2KZMAX&rudGYpV6XEptWeF56uIYOIn!SRpgq2Ei#`mFmZR1=9E!3JEo2O)sjBgGp9P*q?SA1`UWNh1|CB@-u5nkzA=BM%yQ73j|1IdokN3V zWOqSjEUbaQ#6INN`G$kcaDtsYwptFmbIDC$=RaeFG*Oe4M+@Zs8K zn48kZnJEiBhy+`q`HfQb9*1>z3Ti%A4@O}AIRzH0le2)n_VwM%&B{@!9b2i~SfZ%| zC7gQx(s3gqDZP*UNW*)g&wQCMS_aB>GSYOxfwDzOIDt!a`%)9vd z!`{Za{fyChqX|koe6@6A^XJ#g6c_a^XFBu@Wpmv;x-~pOy}W6V`$yE+8YrfN{?I)Q zEBtJOW&%qvAV^9uN9ACO(k=8W$9)w=;l$s&iL_o){ZrKD$7fTn#<3{JpS|-T-7CX_ ziMcz+a;bj>kBknC9n&8tO#8a*dS5djecyW+I<5kPB`jK_EkeK+2av5%OUT2s z^X+Z#Fai0oqxqJNK60DN68iBc^S&R!=F|piCQNHs0MVp_nuQGETT-ouadY6bd_oUt zG8&GafZ9|MWUi>kpiw_#)IY=3R(%Nt1%KB9}z0K+NI=gy?zlz5g_YC?LsH4D#N zd-@0g_Q!AO$Exek>Rqx&(%ZxCspm^OcjJ|n)PCe4@#T4JKI^wnW0`UpNqP@^Z7rWT z&8kx?=FoSP8k6qU)*Fx=bMq%?mA&IBdcAKumUpgK#-4F^zWDVtEM=?VU{cn4$u@Nr zSf8B5z=bVc+^}TRf_`5fDVg0uva!9xio@{P4lC!F@UtbF45;&sHa$0hnw2D?mIpo-@WYip`Nqie(@v33X1 zh1ymZ(nPVb0@-!ewoHYit%iH;B3*QPqi4|*7Th(0@& zR-_7iWz6xGk?6VDk;hFFFja>i?>CibJ1OKW#=g-yMU8!Yi6^2UR}N~H-9b+mAf@8Y z9<}FNhK*A8{v3R--08H@lchj!2fj4! zLga8L&C#ew2~Comglca#?tdd17wMSqt4>vV79z$7H zWQF;hx@oCyZWgUql%CRaO9|+sFj=1BmYII!7ta`zbu;|y74xbH6sgJj@A(TvI9l{K zcS{Prb~q?vJM_8E`80MVnBp?8YvuqIO|p4h9;RC{gFK3^cmJ8P)bU&KS`jfWbqYwD z`K6Rrz4eocsJRt&f6A_}zdNpT1b5>z1it}JU0qH9&yc_K=dUsHa}MIfqKX*O0E!|o zIL{S$c|>Vk$tu13eleO(7}N=;tA|^pihL(iy+-$O7+IisXHn3T_~!7XSlUp3@>X~i z5yz(f_JFy(p3pU<2L$!q))v82>qW4qD5-LelYA53su5nUlf>_F62d!CyCZi&m3&G= z<723OyW_kUmSHC-WZWzwTZ&9Xs9x^ACzy1TVHLc)V99JEL8JEzev#N|wY97DHZM8M zUCpjc=*VjBg`RS_)9Yo)B2U4m6TvioLjoQTYP1bkrasZI_~I2w8ypqxyR;cB^l3N*_q2zO zBAC~I!X7KOUNA4DIadV5?!eLa4=kE^ZO#CA+G+w};rr&P^WY$eHJ+TCAlJ$S0*;jY zcH1J-N>%wz#)X&df?HW%8gjMNcgnW5X-{cS##5v!jRR>|}`Pi7ZFm;|L1@dma_?JGV_zYQO= z9oTp;|DZ-6`UyfByqy>y8X+^tz0_2)q);)Q6s{(Eo!Gh;1>}6*x#CFMDc!>dT&Ka9 z@q3$c+~I*RMRH|7l!~Mk>ICx;`j|30(ek?*$kOLNWjV*GoY1a%FYZk8>IJ3z#tf0e zSA0?oz93% zw3U;9XjV>ynR$&_q(GkUu)mjs!&7g!`qQ`plb4bBf=cFYm0vhVz?#+L3!hie%ChYT zoP1$BwtJJe2=^qLaw(Wm{YFq4(s()AJW8(R_S;Pg{^jOY8IUz@(F#5S$xbL!WDC+m z7wt3J_JjoglpDSO5@{rXMfQBatoJ3v5ixN^himij5f#C9NKE?iO$)-Y+hs7$KNn|! zi`nbz^8<(#0wwtoa>_e%qJ-{#-G? z#=1sIl~UXvHk!HC3g9=e!AZqT{utTq7uEz)x;H=L6DGKu$-Sy%unvSD*Y4>$gEJb5 zHk%0++Pj8J<*0s6P zOhi&P{f>Uo+;*wif&OJ)I-_Q@| zfA7GUHR?_?&wP=T*4Mo_YjDMX?vt8oGaHc5wKGg-lh-1p_Oa*uhey&Tw(LO;ttpOL zc4H~EH^S(%n|~d9+szT2=R6)}#mkc^*ZcF;${R~si>e(IaZh9WbhN)jgMyj+*k8Bx zz{gIUl)?bloN5Ex-yf$2yIsC3-Vb^?U6aLNOXv2`aZVrA&HwB%jV41wC~@+)@nxyN zSzfYF;>yF{h41uNGd8EBidgbN3v{?E-tGXAYGw@I*BBE`&k#lvR4~?SE7&I2zund1ZIKk&px6Yq$)pVw3 z%2^g=dh@ykrAjfw7j4?h*`W|sE5IwUujJ(tO&jo=1Vx&wK#C)?XO6*ziuE!VJw-wx z0grweNDUdAXr3*XU;uQ{E!XSR^k0*;=*(XYHwmQeB|$iLs0wmrt}ojbml=4X!+DDX zAkuq-mK9x(k)dw`V&Er`5+onenA_=2YT-ME`ySdVT6yP!Rs}Q5b8uMgp8< zW7RPbpEHS1U5q7UBfY-MsXH0)pzTggoj4dy@UIr9?Ze z${47{9}jOzoWVGXjc84P;CS1r&5hbjw8eTQA8|gnp|)`J$vsblG#+lud)1kVE$H%{ z@+ISfWR06rHCg`%0Q0+cymNbpQG7F1c5I!{dEWof(Ob9~-C|Nee-E?}oOBXM*>?b} z+>*a(){W>qhaG|pzpKD6_y#9&6J>`~zVCk} zUfAx2cmOmP&;4ap=X)i#S&|I%Oi3&rIn1*^mFEyk_`BwC5=j+VUIt;Z z-!($ua@?i8vv}MwX13gVvCvGUT3W(Qy@=-)vfVLvBmU&v^uAtu3i?|;T`qqN-7&^Y z!-Gy}xU1p*mBA6#W?e=#Ujv1=P{YAOUTuqT?{LSAI*vr1SN~b2HFTBr^xn3P(G>+3 zrCziDChEt2wF3QX7KXnc(Svv9JRT#77c?Yyy?9ztVij8BE+FT_Mtj;2t)!6v?s`Gx1`nzX!y$cmxmlw=SV@1V z7AP7ml;$?LMvJ%P#lX#0c7ygnM#Qe{raogbq;8zt=4!x}3vJbNf7d5CWO0YoI$)@G z*AOD=&pbkyb0CX5vu4q#U2$WM$JS9+(48a4j{KEYOKIOkeO$202X}T0Y0sa(on*`A zY@=t#MER-hH49#C`daYFU3>}{9J9aqVU$4+3@gn4+n`cV4Y><;gV03B#`a3+>(}d= zJ$LVu$YRc57ckh??ruw9(v~TK(QDN2cEK|r4Leh>uZ%6&9>;0v>V2K*bd=m@Qguf$ zJcGBJHVLVo2E0}t#C#V1E>H|2mi@x5Hq$M)PqyaM6iSYgJ7dekIP-qDMX;Msr-|+< zapaVHSy7I-02|Zjeb`vY^!Y&N-})2VzlsS?a8w!M%2+b>4aJDE`;MugjnWEekJ*!K z4B}D%=_XV$4;x3X@Nk z$rpwD*G+9s$h0CCC^_Wf1=I#OVh^HJj8CJzB=&2Vf#?1(#sIg79=2zMJc<;i51)aL z-92w(Rlcp-GI;CxyLyyJTQP54SxKgx?L~b0>%_XLsf~_i2^}0s+WfXgNBotRnqS{9 zENU=)`Rl-yY!&(uenlRs;PNHH_xlR>XZC!`q-`vKrt!ByV>KTj=CNgZu0Q=K(OE% zP&a8q?57;E?=vk|7nAF&V%I}ZgM%YfOP+GWh}Popc}xDGNiwQMNq6Mt4{XjtCTBgW z=ThT0=iLb*4+lSir_wlPG{;HlQ}BQ=#J2G1Zv9F0!K0TKPM;DsGDwmU7Gl@D4Xqjo zma4z}1ZmB~&$5-IzvEqBxc3ZUChjdp5c<*QqBjqk6ZbB&Gr!zLD)@DQ)hI zfKcRtqiV9^0UQK%BpLo#LZbIP+KU3DXg(}4kRYjJmPZ4rfx3llTB)1a*y=qwtRMa! z#XTDYH_@}d6!Z-5f)Or6?G&PeTc&xEXG$<)%7E~Sda;Gni(wLF3_DQ8%l76JwpsJ~ z?d;WfWO;#R(U~P{8H|wE!Ss*(_+5F?%vSy)VXwHwn>~78<_xwd!;j-E8}k1?4RHAY zum99d@Q{T8Id3pz#oKf6 zr}=WVR|?DCzS*wiiIT6m64=UsQb0QMs8eJ0<5B;N1e-FOUJDD*VRgQy%5r;+&;923 zl9HLH)hmed^P>qji(c71i}z8Q?6J+_tI9LfNnre@B5%x>W5GQGs%%YvtJkLm+3$G) zyD)mM`v$k}O~k9|+0gu?Cv7P1z5}mgN{IoO^0t+!uYYYLL>VxGs|aFO0;!XwP%S^y zQ41?ozRuQk^({t#`*FXw=Ath0OOTi)vOM+3KtR8Gm!`zPHz4=)U%StSC*sa3z59^9 z=s>^bkx!5t3$g1*O@4hDKDd3ZV}UC^O$}6K>Ols)Mu>|CD{8Avp&U_)ST(Co@niJNiQp{c_(8xD@80n2XnwEy;Tk-x5aE?Znqevl@CK@t z_Zi}_tnVe5VwU;~m?O%5lNDzQ;We9@0M_4&Wo7jzK>)h`Xhz<8^cvrrt@CFHk76P~ zbZX`A_@mR^Bak^Ps5SU|hX?vM#-Gnq26EDCFZ?xENF0c#62!k+aW&gG(ZJqS67i{G zoB@sFQB?%dG@!y}Z+0n8OmhMyg~$@Sk(RD4X+u33G1ZI?C>n+=Y-lF#g1*E~&Z0we1HX*bD09qODv8e>!?l`YZ6^`$M8?==M?kbm2kl+>zx zF)oJ`Lm)p`1&iq{d#5}1MqDD$eeFqFNF9ZO}_h#vXavGIg>5wd*bf!-lHgBDC?p`;{`Ra5cv znT6cXu6mK;68PP*mRW+ zlX56XpK-KBsePPseWvFSUtO5IETC`=*qN^TPTd%wg)AhcqM+Rp%#p8fJJ>> z{AEr`ik6x!Uhn&g;2yBf!QOklqEyK%8w)vUKRZtyRI~0OqEkUJ!PR(| zFYIk+vEAK|kBP$VG%KKm{_OqzLSGQKahcvA>wcHc_qT$2|5ptp9Bb1&f!0ZzmJO7A z9(4fX0HJUxS@S%*j)p>hJ%%>0lRU$za|qMImNp?so@G@`DWdMoeo--kL*5t=)C%-u zSnajh>@K9P6V8_OAueZ>hVw7;IcUnYFSvkz{K zc_suV-kw&C5?;%tLGl%A_JdtXTzfCvX^mq<{FBu1i(7KNtiL+BrT?L})3?%UuiV5J z@ZmEK$>Trg*Mtq(=;O;2K&GLq_VNOTHaNCea{>1(FKu44t&V*(<23AeJC35j@}RcQ z`0BXFaXm{tVKntqPVXtNtnYJ`CrC#~M0vTjz zw|nhv7_|hN38)pHsif-(IjZcSma4N;^ z55if$%8;o9WBAY6Q+{TJl4+cwsEd1lHPT`_4%o5zT3!FG0X7a(aq6Ah*`QlX1nY-MRb-;OjgjS*ysM<;odwtlLBL_$PrlA$>ni#&WNSqg__;^z283R zUTPT;$svdOlt8zGoZpLQnp3QN+As8!rJ((=t|%mg(@Aq;xLs+cRLOuissH2_`G>yk zx#?;{<2}B<>hN4ly(-}0>?Krxk;u%GgGuVn&>9uOWuoYF*0BD$d#4N4?4>K|n)t3C z^(bX%P9(z_ef}voe@^rJM3DX56sxl1L^%=0r#m{*N$ShGdF}mb5>ljSwwwsb#Q9l) zL&2ZmO2(;tDNN{oFe;$oH0TAE36hRsZ7bj_S_@l~86K{Kh@uONL5>hoL{d>PMdVFi4eXxe1gfW+gRFnvL)Ls>O3Y|l{S$WoLS zbRNd)L+`R>=6+;s1!ySf*+lmx#FJu;83$3{EMhn2T)3VR6I0$}YQuwd7LgC84CU;$ zQqo1qswjV@D-5cb{7kZZH;Ml59fv4Dhuw{Z+@O86`*dqG0tB9oU$p+J*A*vIeN7*8 z_1ju&_LE8_qi0jB2vXMG9@?i>esGvaWZ3gCTW`wyxV5nRLyi;;u3=3f!Qs`fR~?;Z zWns^SrTPmZp7+ueeZ#ig9Hz``4f1G14sW--uYboyPr76z@2YKG1+ZAHE+}n0;NXxR zHpgb^?kRnL5_@uwLeD;JH84{B;$lHlIarvp3c??gs_K)wPep4YJqoA}eg-A$p4rZ= z34x*13AO~cUTbw+4kYJx;Z-k|_$Mw|xNJw>eW(e8_|4+`>ap=2iSSo7g0#NVu!6?l z=k>|+vOs!^maDy)qiL_ejC1LkJ8Ao7Bex;x{de=9(k@TS9>4jHJiBo}LJ%eA&1dRrPm8)zpBf>(1j;fUF{rG4gvV-X-eDQnxgoT8*$@r8;8s~kZHYEj{RpaCg% zJml)RqG3rA`RNrCu`9O0KbT+CZJS?w^9!{3#SL(K_&^CBNb_yogK3Moer3TdFlmW0hIXFp^2Gcb8wXj2k%&+ma>eJuMkOzJ z<9hO?@#$FTUsN8U#HXXibWeHJ=l2k)ulZapPI$nHA6q15h`|n~1aUG4=rYlQu9zcuEAA5nWK!Ftk74`D-p%{x>Q>D01 z87(%|j17>i;du7JD0YA~gt;}0x=02)3(3Z%;q1nRZrYi}xkc9_PWq#b5Y~_)9Zh*k zfs;QU-D?fB6?NvnjZzvJH+GBjw&@jwhurahG^FqZ(+-}`7KkH-Q0xr8W*`VFFuiN2 zuD+!hRB40(#O$z&;+hu~@?CoLM;zDL9?Goy?-!MH2&FEgAWjDlYHcx)!wixR9g)i$KI#I9Zy;Dd|*1{ZBhA>T|R$+ z(v=aPGT;_q6HjPWH^Rl@v6!J|C7z-_p5dgD{(KZ>(G(AtWm~Kc{zLxS$t?Fn&DI3V z5wJQ=olq)o5~86Tx6gZjN*E4J7;f|;jQ&3WOhL20Nv+;4@iM{H(*VmMWn80<{o22w zYr&~Ck2ykkpPr4L`@B`Wn@V7(+lgbO_SnzM#M`Xp2|cz#B|*HxmrlphXuO_yIZ3-r z$~-`bMD|30btE2`csc(;o7RNoeOP(;xiKTCA4GA%8Whjcc_RBp2Z8p>a~~HqZZtWv zu_E^6E7%y1efvQ?nTldfB8&>^Z(Y>4n0R@B<21D_1nb>CObZUo#I0guU*W$79GF2T zzk^0}PRPM#WCPvYBQ%m#+a>HL!kK06zl0mNgQr6tylyjNBdN?9DM;ku#0TaWng2R8 zIwPV?(V7WgJQ+C8pN(zKEbK7^NVNp9%Si+q^kYfDjcFbmrqZ=|KNQ8fL;+T37hzUb z4yoBF76yY@>+@ra%Zc4wMu$4HsL5q=6Y*-W+SB(jledD$Em5p>=ApuwMgZc?e z9-P$sv7SCJk{nnwfH9aE{0$vT84(Zr=6S}S35y|;-_ZGRsFoat#RSZe1G4*tUo)T2 z_`Ntp|HDd*Rh}Hp!I3|9Ea`)=vS+gsSk23wB{{M|z6Z2#juIe2!INuDLQJmL`*^w1 za3-M+f9AQFjH`jGEHm+Tk}^NwiD6agSlX5H{%-}3212NCXW;`=n244Yx9sC>BckWQ zfy>sndD2)KzWJp2%kpxtk1Yzj4QJ`TT%_mmQ=5b%!`k~6Iq`(7r#fuJEP>j8`>X^ zMlmz5lt$4aZXiNwpwDeqlU(?c-_t{%@!fK9#V)c4_}rMFm~a^Uy_jI4ZH1eGaKdCJ z)0pFgIhl+VhxA75pn<78_siWqcLGli8%IQu5@!2xIyMfCJAC!{FI(6r$x4k0jEI>_ z{E)ePOmCsDL8q5|3=o|1q1a#qYFz@d9=Cme<)##)q?YG7k+26(mC$xeJN+&xzcAo_B3^8dsvNvs#63aIa*-Hd2AR@9+DPKhr0Uz&p7y zyy>!wRED#Rc1$HAeK&Yx-0-5H7rP`<-!?lcERx7Qz$H#PsDpIssWZ_f;?FG(w@~TZ z%|@&xvu%Udj>Y*m#uQF19EEww@p#8O8gFFXNUYc6Z|}Jm|K0xic*MU6#PDo94=-6< zSY~tMNHC0M>MVQd7&iEwG=L2p*~~8%j_{O>uVFG*XADF`ICZ{;$kx#^&_EF24DPoKkF- zVsx%1)M|}ruo&dX3_3n8xg!%4t40%@2a&GMCWx0!bWS|Z;P;%O@w4kh*Mbv}v%1{n zwO|bimY(~)RlJ);mYT^ZB4_w`J`mU51gvgNWtTy=w0{53a( z8K?Cj>Jxo4PqWIKi{q|*`d^obxL|+x2L#Tu@O$2;-i#`56!W9E<{sEiqKXF%{y!cJ zKP3|8G??u+FUmY|yhb9c=WqHOijDn=8lzcsNM1O=B*%GJ*O3aIHS(+dHeZIvN0j1U z>EVCh6UAmv1S_>>k<+s(8pm$VZe($z)}KJ7JBHt%<=`WheX5yvf}D2L{c^;Ectqz;Pc}1BSp&zUpPFF-A7DznHX7C zy&OW8Id}F!cGB;8i7)&6=LCAc!s-|FpLMAex2<+m z6KZm?*NvZgZ~q~C$>$?7O{2mXK&{4xP6Mwhl0+aj`_MREww^}J6Ew;lH@eZHbE4Ji zLp3SkDmsQ5BHW|YF<7k~qX(~rz#YDme#4(R2{vc-FE<)dL7o3FjiP(~Uaa%Gu(lu@ zGm37_{JB|7^|`T{28RuS_~+F6oy0FC5B4XEP(x-l6D_+Jxa2pN{^8FYnQ)S>cMILu zW>!>YUJyW}!bE!=__Ih!NGeYbBfCn@{5|~-tKtmtGS1N8%)U^*2hCijhL_ip#l#^( zJA7XJ(mt@{`%2W09SJ|m-8NLyIdDQ2p2T6|J;_l3L>h8dF!rCy8Nk@5%25Gw4g^wO zf&gXG<@j#?o+rZCI$Y%Led3&rlg5g^#|w3M{~t&BomoBLo0z5FOv+bC!Q%kMkaLw zUHev@gcBy|GcpzaTP9t)A8d}-l9j`YCmPwrRwQ0ofyw2@ncR8uj_wbUHQxtLz<$u} z!F2b)PCEWPV8iCz5Nd-SVF9pvfuj?cd^;_4o>n=Ao5`0eI`HBUCfp41aycHJWaM@x zU2f;!^LNKg7HstguxM28Qe@6A$j4e5z&iZY|43x;_2m9{#$o?L;_V3~;^o)#Bxrbo z^+4E*rI7%pMQ%Opae>E<1KuDSX>ji_+Hk?)6n*cci3TBhK6pU7tTW=Y&5!-wTU){S z#)uN^FD?+v^m%amke;27>3lxr{SXhGq35;962zv^osDi#Z0u`1Ic_xGjm5FVAYn7@ z^0X~L51I`hXdU=eFDFcI_J*;qMo7462#XPMwld?6SipZhTuEAlt ziQEY~s1scBu!@x*KG}?A!X<*G&dusgn9f85ObF~25!;zay;;q|+-)*jMYUP>*LBeT z+NBT{7;IR0-|ZFZ`zox+pM*u;0?dvMjH@Aog=8(%+k!Y}_XpaCc+NRzN|%iY?j2h#Z2=c*LSZb|*|9Kr^CI6U*IR%y{6dSEXE!siniP zCOaFu$rRsj@}SL4|LqK+Ol!bS`tLnNxQB^&E2tAN>9Wg!6SCJ5PP+|PiI(F^b>h7s zlB*9BlL9MUCW?A`&%fbF1AchUXD{{eBRZ-Q4TWc3c{r}Q6ZO}5q?gNUrorv3B0RG#ci<%v9w^|2BaCI$6%d5 zgmRMuC-rtBOsybZ?$oScd7`b6o^Y@&X$KO7ZBJ2_H>Yay~azn*lJkWs?d6K{rm`S*O!Sef!`Bdhm79yWS! zF5K%qTLQbRHniI4-WrTTiF5Vpj}0bqEj}_ZL2B?@5;_+R^z4~^Se?9iHv{9TvBNMo z8AVmVkMlI({D!OuzCS$C_9-3j?+91SE>zNh!4vR3f8SqFZ0ryGr9_Hw_(-e|#Yy#& z!X%~JMGBbX8F?b6(jsBYZ6pSHrEnPLh|huCI+>ZNPt(4)es@D-;Q7F=r>Hj9AMtU$wCaATY) zNJ-yESA7u_8~X#_AhZ9O;zFz(R)RUS+hd`Kh-_e!^(c*KyXdCv%(*o);vdI82+j?A zecKz~dJuo^w&H(Ddi(8oC55>?^`&0?{}zEm0-QqjYFgatEoeAl?66US=F4SYbpC}Wx{12 zI=dt?iz{wayz+$9HC%N)5Fs>?df7nV_dM1+i6`?%V_wb(tScNYk{Ru#4qoSw&`syd z1Bp8+~pdkyho1V3jX{nfKgJKUe`8716wI&KtP9q+h$rs4{;An?N>iB|m>6Je4wlPm1>w zBG-#_ZBEcYz(S0z-x_ZKhdh29^aN3vosWG5Bd~JJ?OA+3%!**=_;EO3cjFXU*&ow+ z^WgS-lNP_X>2R9*d@Y?TFWv9?+x~)LV}Ii*BC_p%FAiI*q;@4zL|h1y7biF{H`4br z_y5su$Lpy)EGJWW89gvZ+WxlxI9%9aGvSplSb6xb;V9mSCa^A%Lb*SJO1kc> zj^y}H*@P5JRYg0;vC81kDj6B_B|;GIulb2(#Y+R9Nss-K5vzSxEFIoI;e5A>pQc@S z(Wk{KBBLr!kWSAwhg`9;Z6W1$j5>Z3kuSHin?bY_@v;gQK43bpZUXx_xw6L*Ul^Nm z^6*5WZ}Jg8<8Yv=pjf#2u8>8-RVA(u8_SSx#-+e62}Xu&#h`Z;BBF(!{Wj_soLl;w z#)gZAFo`J#wvW39j}_$LhZB@tZ`HYJ93`S{p?&au?~r_G3T0szS&kdy39P2?=ceLV zT$qn%$by(Xx_8Tx--tx8+2O?{;s7{&;`%aJ0DR=L<3b-@-Mfk z5><5J8|mTel>Df$$YpVQZr>O9Q7jAZMN-%i&B0E+O{8Jul~7Eg8Pa7^W#X+eXo+Zv za0!)U3h{exGH*lJ znI!UM)yv^KI$pIUKlJ>{@xNb0ybO`kK)jdzy9^HSmx))4ebn&}CA?VVwBkAI;3$sy zR)G&&;~t@US#>fTBweydJiImYc=9fG%E;^k+Fla%wqi&J!doN#A(PZ?a-V#7ZRuQ&2gIG$| z(c8WtrjfdTj`-n?`)+Nb{mbxpG=#&YqecW3lXP0;lv%8O;tH1r13LcBjKyA2U1e3 zl&=R*PI0GiCE~4j>ad?WeiM=WHcu4Ga|b@D{$QC;sLztu_-qd!7Jx=iS;&{+Horr>fb>fuGA^c5Z6^&#^9btd8e^zKcPiEU{2aU3>05}+* zADWDBSAgHsLnlBaQ2#l|Y5X!^(q$6i(6ZH}Bxa4cy@-Ziju?+Qkzv?C590c8F5Zpx zZWg=0@E#Gsfk>K6XopA!v(^9A@uhD+crV@#2e85KMH#!6Sgjh_=gFkYE(B%b{mk!6 zgB?%6T@O(gjERZVR=Z0leDxe@(eBN`RvNr|9>bl&v~S*zRrCzJPsdnpH{zt%DJxj| z%w*Q3i_mf`?HM{&0XkkeZRa%=oC!hW!GXz(!^t?+>tT-zJI!va z4Q659q#Gx}9*LIVX=ff@cjjYuxD;y&C*vhg98(|o@}dHLi5a0JUC#izjZSe-J|e~Y z2|Ys}nhj{RIdO}{&gOjj45?@!B*v6!2hMAdJ`S zUv9Pi@>_pQFUsHbCth;;&=}3b6@OAZFYN02Em=(;=y^q?8Lip9pTN9XC^q&tUUPcL z)bQfC#fesnN%-oRgqZ|SlN#pa+G`vR%E#4dPb}d4 z5+r!Il+SUaj>uQ{8S$Rh8U=VzU`5WZ5>A5DO3x{~Cytp-XdtxNEvWO_u{G$x+H@Q% zhLz&+uLva@C_J5=C!(7UTis|SrGJG~Ki`i}EHq%7%&2qSxp;8J#{R@FM~=Yk>@>Dp z+@dsiquVJ;Idg|%5)?u9CJB2icC0AqTR+dg(0*BM*Cg|(bIZ$ynLIPZn<3vX$MxIF zzh_@@x5bMCb|2;qyS>PURb-}>n=NS7)3tEeus7_%OD+?ArM&$>;H^jkJ6tibdac}O z#a55EMMaYwG=S(!P5}6%{-MJ96$Hq^@)K~#_dh$@O;Td!8&{lk4AWM!& zw2M^X89EvGm*`6`FaNZsguAC!U3pyhy zlvy3vBa!72kBJ!W#mP9{%}(O2;U#!!T;C21eT`rJ;UD1T{6bVsoQ%%wBAj>l#9;WL zL`vUo5-U>M96{`}gs|`ylC9qk6dU^!k4+qhryVYAbw_Z(AYm7&n!`N8o2~R<%Hb=# z`sWZmwA%~%Zc-tG7b0=2bp?quX~fexi8ycc+ujP;C(IQ!n0UK17V7XeydS;wQFBQu zfptbb>Ph{}zEApZ8jY8e>GtB7fz8iwJ*I||*x`tv%IXt&5>`ZRK8FB@qdmnd+=>dZ zgOnCqIQ-DF$<4FMA~cDRS<#juUO8={{9G{cX2434{p&n#93>c$ki?n3alcD&`w_^UN+fzvW+7f8cjwaIzA&^+J?i) zcu>ixV@_juj@nQ1^`K|1(P|{K-h^_p4ANayoVp%N{ z5LZ2E_0qj|25~fzhXcu6tn`I2lPu9UBn#f~*zsn1@RZ!Y6}(OZ#qsg?;uu-nr)Xfi zMAyIDD!U9XTU}^3dcD9?t)wwPDtnNmvm}i13|@ z)Pb*8uYB*Bd^5gWCf-b5*z0q|a=XZJON1tJM!4xdO0orV*<>N5X(J*r;gHdb`Qap9 z7}5Wc4*xe8!JNF|Snp1VoUx76flqsVq7?dx9>mKL4adohKS6^7`%GB{b79Gh510v; zUm54UAEiHO4q9FC>1E*!_4a74H`o=(N_+{ioU1MN@zDV^um(qfc*+&D?sQdkybeViuF z`PAUSB~u6$rZ6@QpFo={|3`13*x29rQ8oAF9=j>ct+(E3I{KeHL zuE&3j1@T5s3X45tzIh#Jba6~4sT=y=7K;TNOjaxyb>~#uS7CZMh3(W?E6pBMOHORd zx%J!{f8mW`cJUaj(>myySwy}ICSF#rOuRhsWQaGD-yj2-Y)SQz@)MSZiL52suE}9U ztx3)#cR_E&?}+q1)VR=L@L>z7mwBW{-@LDXXV1SsS&p+^R6HE-lR3VLj)PZbwh&RW z@048*8CQcGqQyA^jQD_FKcvqe5H8bq86PkYNKDqObY)k})qzK+7rs`G{oPH%W%1;j zKd)a$v#>U+2=&o?8kijTSm&c-Fc9&WNR>O#Z1Z5R-GjMCGv1~_f7bZH?K){pM+|h+IcA8LSMPDM7S2We5s@zw??nPf$Osn- zT?ft?%@bZ7biN|mL7W@=w+-}LGi`4|jmd#kH<4T5PYOoh-#LAFWx^yZE*g$~$vo8C z{AkhHQD=~F*lfdgvmL8FUMwpbn2HQ{8c)#mJnRXPB_Yv(XTnEBy1zBp@hM%~PxM}N zIg(f+^Zp*%}l(k+F14RY=0#U zK1+N9S9PK%rUw()B?WNAkuAr` z_9R|bw3q0Zt_b4Q3$@Dw1%Jl_2cH8k4rwv#1o%A#z0E=;y>2e@G)J}-y3X1ye3@M z+Hl$v$13lwl^D7eC^p^+e7{s)Y|uDiH1-JMB?V*C37_3j(RuW!rqO6cKU5faHy*$y zvq|i$H;PxRbhGM23Q10e(m`mk__12@;?tvhw9f2Zo$JQAQ?b0I#It(&QXt%Kl+C+s~46W1>1n7Q;yz#Lnq;01<8j zb*Rm^nE9?Zcmodm6wvX%1&C`!_9JU%D@y-T+p{{CM^c`p4C$*m$SW z5cH$YOeQ0BzG`~t+U*9MriZJJM)i$WA7=G&s>N3w7SzTQLS6TUcsZ(}(WDczz@mU4 zy>^iz+#iVH3CCBL1DZNau`HC_ki-QTwha=r{N`kdGu4x`cRKo^&-VP7^q zYh48PX|rpAJ9~z3GZTz_&P=?#DzA;sgBOT!qz4Z~tb{qX_q4%;vw9QG(Eo~D7A`l+ zNS&>WB=H!T&pnSt#pkgsQH*2WTy$$3fYm9ikWUDlE|MoTkGt$x7jfe0d-}%;P;9(Y z_{9i0=Cvgn7fFiQf7d`GeY=H}0X?X^YHWwzfjKwnGrd_%b2><6x*?bRxqaege=M)S z;*P~Jse%}2px8wJJMAk%tzIn5#Wb%+BnIND#;o`cv)h8spF0|>x)!ZP__^N<4vx;W zAeuy(BS1>bDg4{(oW1u!uk*T5@Ab)f9ISTXr)5RQ1ba@)5diXay+*!rIGOAxH4xz% zNI4pb$mkx>GgEKXqe`OtL)Vg*-(IBa$*UvR-Qk#M;X{liX&~X$kaC^_Cf$=dt#CVV zy|1%6JxlFnXsr{0f zLmBBj!&#W?8=4_rcN$Z&i*X>7roq{S-;q`F8?q!g0sX(xwdW-CypU_R$3~kf|Gu|S zY`jzW&u&>Ul{vhm8g)2fwF(s}Vmu9Ix+p}9ffVL?qj+M{c(H9?U}-o)M8~VwuGa7B zL8vziCSF!moTe}{`EknRz&`3Ai%4Ph+;0ZUM~uNze;m7n z4bY8Xe0{QkX4fCt66+Z}Ru?!hv_`|{m@Pq#40co@xbl|+4YcFRKd{@vRTLzRZK=qybZh{!AAGyD)C;Rj&xemW2>EaseBy$ zf$6+L(i=v*K_bE=l}g9R#LHn}40+kD7EN@&`B)q1{vV@b<%u+o{XJzgk+L-lfAvL^ z3zxV$Hl1S&{Z}_VyJsXHI!K-Ir0%777;V8I%Hk=^3H5yl=G}-l3W`Oh!cM0fpW1Es zfSwh2?f4B}PXb#Wwe(%j-}My~8}B3*6%}Czsk#b!FwfXsg3x&M=9*CaP#vVsxR!Hh zD*X0#FlGE$tV?F0LT44(qIwg@%f!nm3wdIPSKYBH;i>_g$6zbn%tgs-O7+PR`S>A? zsv8o8*l$l@2c2)T#zh^AbCMc_>CSn7ISD)ew_Kq?hrK!jmW2l1-SF0=37F#zhzZYD zvt77_*1KT4EF>sAF`RM5 zu-eo=e#F%4-olq_)E{<-7dcGbz*5ejxs$F zFQK=Jmy0j550T%qI$`C@ah6AHHY}q~mf7#?;UE@;$y9e|6WNFH8!3$s^%3e|7WzGz z$3(n`S@jZOaMGU4YN9gwZfh`rrIQCPrt(koQ+PZ#2g?#k)CXd8{05QGXp752Un~EH zF{2=bmzbc7O7Vi;i)ZXfyiqz4GbcWPnaMPkdji-__q>v>LARMJPh0U@qgN=7kLkQK zeorPQCfy_Kis9w8CLNJ4St~?V+|H}kn9P`XIqf3XWD^;|=(pUNy6Kv>dCXX4*VADs zA8TLWFLKg&cl3D7bO*4Hz{_lzLUJA@^Dmg_YyQ|FfJVW~0q!7b555 zh#x1(S~^RD>6BT*Sz8e08V3$W`e)IIV&k2}Us+99nO}fCdb>z9*kUt@cwU}V;E9$d z$&JoYeYb+-@BHjDj^KdH4Nm{qGu7j*fXTL1ZxnuBE{4gfjN3$>RjVDl=>4AT6t)a2 z5S5UtOGe|M)>Wo~;qLTjSdg5)i*s#@L#9V*x>u&W1Z;l;> z#i59(p(c)-j&Z+7d2;QA!r_=1j9@JhYgHm28>~Sr^T~0vFBVP0_etTtMA(u)0{b~f zGMUb7f$${dhotOyqKwrpcL*lV79NaSMpQbiSnBV;*v#k0Pr%9%W3j~_Lyg@h22`H#%c%G>^vtjuq1|jm zGd=h1W(zKIz`n*y15T9AJ%tuy7<)AytT%_SB9Ma_k%28=f4AaLB93aU31_(qhu(}% zzZXrhAa+Vd>fq+>qimaxxe5Y?Tp-{z4C6Y*3c8!Liw9P-6b>rY`3sk!U7Vf-wShbh_lSdo{D z4Y4pi>;VzT->j3%{C%Yec0yPUgLRCORiYoKSPk%*Tg0oadvQDeh7 zQqG+9i?e+-(|4x>a*6p9Rte=q?0bjb+{(h+!P|6P6=bHiIs8I>^8molgIJA5=OW6O zTg*5nk+o%aqQmN^0mM&(tsmu<2)4SCSd+SYOGphQrjqzsU!0GvAre14$w}78c^ddW z4X3e}sq=A|nkBQ)fF$ zorX&T?aURz7pBm6ODsBU%Zp&S*M#jZEA~+*JYbaxxSGt*diGrssj$zO ziMN}~)-qE5tGol7Abq~ai)uO!R{5vgblh~loOhw;c0VBXuqKs_J>D=@5dkiAd~qGU zhl4h}5|OYdK?)CMMKKu`<}*z@IsyK@KG&wZhxbJ#44r;5udJiFmIN z@m^qaU1z3&O^b3T-IJgk>c`<{=d>YuzH+dl=pov@@-g)Wl|`dO_O=Gv7yF$#g5fM3 z;{`gta~w)W*RI1vR-Mg;gB};QMkAQ#_hVWx2=44X|4&8lur_BjR+^(I4;SGeSy^1( zzSZv*DM{h+)1YX?@94W-E-!Z6dt;&9KTP(0{l^Rx8}Bs!xBc2AO|4yaIk4}J6%MP( z7BX9RlS+KW@Wqh=w}PK|3|N&6VY|(QGV0{4dU=wKNs}k^xYK7!0kQgIf|XN35=mbL zZ^NJ2UBUiNj=`;>V`j3H1LWCZug5m(Y->V(Opo3;vGY`x2fqrMFg+E-g0K%O{T^&~ zx=8g(WX4WpLecoYlV>&6eHwp$Of5RfX8_n*JnEd1b|5}d~3k;tR zx%tU`D%+>UL@}XJ9CD`H!q2A!e8H-1R z#hs?-tBb6e;}#Q+(coI+aALpPi_Ju~3u#b(E+>gsALzTfd~!O4#ZN;fyyX2=N#lPlMBf zZ6-ff<@Zgf-%lJi?8qy`UcD8k^$vVM*O~{g48igw8m~%Ye=eKwTot#LNVUpA*PdRR z%%p6&!UbJ-0kUQ|;)JguM@O6{^?A}@K{E|*~%HLTw|Qvu2TSaiAk! zibK&{EU?G|ZsVl>b2)t#6dMNuKO%*&%$G#1#V`Du+}X|%k@0NxNm46qL`Hi_$!u`< zeirw|!hEp5XQs=AWmXA$3>-787sPvp{f8D(utY3-=`#~AD_gk=g_%g#Eb6gw8b~JN zMmnxqB41uMT+b#kz4HHXM{gk#IN%Loy8q^ubN;g@fKBO!u;HTOY!?kc zd>uKcon1G49e7a$`~R7IjF(F$;d^=4Lf>u%&qcDaCXkIPUkddm z7up;?G!x1040`bt?f>O;5^sL{8?>qN?^WDB76r^$U2Jq(SMgqR+JIRu$tybf_A$X zWxg0z+`|9$1^%C87_&x<##&Om9kl;$Qt|8ps&Lt`K3Rz8vTm=)rxJe16&7;xP)X$5 zs*@ns`H}}Oh&s8F7BxE1`kuoN>#j>o)Y4tA83VxHTD*X)Cxx~2(4XmpwSb$>&+8s z{K^WJa~*J!8IJeOfU|vFyh^;8RbpJWoHKi|TY`yKEW4%qc$w}SyD~ncdwE%>6=ix( zo4x1`Lzdmh_Y;m<~m!S{=X;Rzz$SH|3$T2`Tqnd8Rb4WAD?N=vaR6ec0# z#Q~28yDetaTsN`1Dp82dWR+z;4|XUv4g~(IFN|Nf0yvO244de%+T3MaQ%<p18JRFJUXjFgvUn2Z^;^uf@LxR=7Tz}zb9`>>@yOZ0PH5aj@ILG_$1x*# z=l#2kSM3heC38`0=-r@_2cfro?r~SNsR5V*-=J{c9Y$Vvg`=f$K;so#iJjgq*tfmfe)G5gmRz$q)OJuXX(rCr{TgY{LvtWPkW={kMwG!H_P7%V@ zZ0AS*r>f+%g-6NUt$Rd@T41qlf;PZUNq2&Y7K&PwO3b%u`gFo@8wluyvnef z6!aM)-LnKv!o#b{E?DV*tn|Irg1tHu_QccJH1U2q6y@Vn(RY|Jd^p~x`#mch#Cvu- zw&h<-SigtF&>?pW2V7C?^HV0xte!XwuN5Tl!|2e=O`+I0Q21Urf!WD4 z&V1wBs3EmiL4;drkyk9S`sI@12l^pD?<*w*SWSe#T}LWgXA#NBdD4dcwj5b-QV=h7 zXaW;2ryUfFC5U*LjAb{(jl|2Y2qxauCNuU-x^;iyKaU@azqd(PkvANx%mGw;3$WKt z>f9K`l;VLisW$jRIAF4a!~V9W`Y#0M?V>EKB}HA%<#P=35@2>QFxcnI>X)nWHq*V} zRGP==8EGPLm>Ux>yBI`d0}(cd^>uTkj9yMW!>|0Ax8s=cG7mjpDG#=VUD!UX5KlR5 z_<4?+Y^J}#kwu5%AslvEQQ;-Q6OW?G=fh6D1^cZY)W!?2+ndB%Zvv}xOYzzeC0QjX zHVzc-qr<8APyY>g>Nm#p7WBGLj!CDkt#1zX{7_syv+=FC+l!YUhZ`5H{#D`y4b@Wr^|GQhD*f@}Qvvd^7 zC*P0DlOMn_k4MDOwp%!bBAfNp(QGcdae6EcIB9d`|F|7I7fxVvER8MHNozD*^;j=R zHAB4o%H+z)aE?$X=gBozwE|k5AYVbeq_E@!E*z@F<%RUvWw2s}#eq#D`>$N|oxtna zNo@6#`ERs{L1kN_e-{Ew&v9a-Q-=np8Qm_6h_B{*(n#RAURJ*6O*(umN%U$GIc(39 zRdka5^g1gVOb)Tq?kHJFb-oblgCSJ9+^F<g;JtjQX?M=Xu62zs!`D`6E4noA54=2^X1QB;cS3*g@EP;4AX{5>hD>D0No zu0NZzbsX?+kQF7H{bwvLRO(DvYcpZ-_`X{@J9F$LtneqWQ!k;BI&L#PARIfJ$&kgb z8RBKt%So#lyl8^M%Z?HGHj{ZP;(f0YFXxPH&=Y|>Tq1|+LZcmX3i>C_+?~MmL_(xH zY$YP&^7HH5{gG&f6;6ciSjQSB{32Dk<}ws9v-Ck6*Ryd z^aZfd>&4cbG~Tn>FxBhATXd}N1;|<|&c)_Y#aNaV#!IBQdhS!jtz%lqg9VW=HfH63 z^APW(epD4Jz#(4>n@wJ^jC!qyzR%9Z!ej!k`<$4P9y~={2Lg(X1BoAJ=U`rLKDN01 z*snF9-o#m^SV_tq?<{vVtqD6UMl31n+gzmYJHt2_rP2mn#06WRleV@#(*GAr$jN{Aj|9$5V=|%awJBlhT{P>fS6dukB;0IARUY|I02f)FEO?d@aZ|rt~v#WvIc=1K6 z#w@_K(wKO8z~~}Vn3eN!vl$0X7A$ak@nY`KETg@Z2Bj*x_MEu;j7du>o-B{C{ga@5 zemood{VB1ktVtuG!|2B`vrlCHI&2T1(iy?wU=|jVg|af1h1J7G;z_p~e{bo19XJ&- z=G;3O^Y5RGX``=AZY{_gjfJsNEb$a!Td)*c41QDuatzdYLtwaO0U;%_a}e)ts6ey4!$3VVoCNeZ1JQ}?ajuq zP#!AitzGqBRA01JB&53sP`adJ=r0IJw}{d+lz?;$-7v&}bc2L~5)#rm)JS(p4+7F1 zlJEN7zwqYQJD(eSpR>))U`u=paG`Xf%57(dR}?Y@w<%ojrAWlx0$Dt8fgK4irmNlbWEJ(Ps21!qU{b` z_5yZ^#-qYB(jmRII}Qo~)bjw5X?4aHUQw2TXfM|qsPE3tVP@-_d{s3bkEU)Yw?YuC`zOnO}Q0{b7E0S-?f+7(DX zUFh~O(9*EFIg}Nli@$qIU-VsHaB11hsdt=rQqi%~LB)%!E2PZjT9YdAnDO7!!FBA) z<;A7AeC>O&InOE{6Zn&56Oflx!Azn;k$6NAl9h;)waK>iK6e~;+CqN3=v}$QLfL=~ zkg*59f*XimLrOR8F%-Q}8dfvQhc+&u#sN==MIZZ)Qxc zi?Lh)ob4t;KQOw|Sx>5$&`Po0QC`XN27Xr=*r_}o2Tbm&fxaIuVJ;#ZA=|R#R8+Q5 zC)OCxWspHuY?bMxcRP%-A?j7sQscAVTg11dc`x$d74800ndEktl#q_Tgxb%<7C{0` z>ieWa37f0EW3y7%pE`i0mF8dTk>RTvSH)fS*Va_lZB47sOE7;ca#CktBAoU-&h~4j zqsYa|U*ILjOnyo>@LHG_s0KJ#ddSlANpW9S7DmGxvg+m3!jQPk%lhH#bg|FYO^FCE zd_?W4-vbJI%*boC%Q3ai8kwX%+1y-gSt&Xn3BJSwYdQ_1zbBqoKOx1V>^~NU=FmbG z?ph=Z0O-t~xMkR3CgjMY14CBOSU1vKh@^gZydg1mLY6?0_`8401PHMP1yPeV<~xL}gb zUUw!{YLF21+9jY-+EU{;RVIooViF?rY*$4g@Mmhot4_b!lo2 zv+Mo)bZv^b4@an~E{pEwE(cN^XBn3c&V>h=5>UdOi<+s1Tgm@=?q_&4Y-XqP`75}F zkLqtN66?|J)YMXA`{rVo;mnWSJx8KaI6$dX zA{;|bl>pycWmWJL`RF1FPc|cd_aaAaGT6=6G#^lP4tKa*kN6LdKYS{>`o7N~;jt8% z$bV{v9uhfMQ`@QJxTLbUY-5aZPyi(NSz;dcoD?Z{#c*}cdEQO{aBAV;2OhXVTX+=I zW}h?T%%uZ#1Fvl}{f*&&zLm?Tq@R7Lugqk_bxh$c6(pOdGdc&Z+cilR%&1rms=8&s zvbO{FZF?M8dL77ncuYtXo2M1qK}mg-wo>Pejv@s2A8W9Bi>lIiqr>W-zSDf};SkUj z;TD?W%)_-(1c>>CV-#=Ran?qSk^StJtayS?q4AC~AmJ^s3D0wnpSH?%3*q{Tt+ys_ z>>aTp&+;yO#;6F;pYrpXV3?!x1hB7Q{Gj}eo4p2ZZ0s0B7?yDR@iW6jbj5_!&m8fB zT;UMXW1$RxV+8wDw!e|>!3~{N?9bxL`gzwOJF1PLY)^J(Grdm9if0CVwB5u$^dak> z^l_5cj-)*cnyW6tX8$4Izu1NQYrm}nt9LkmxnS_6p3!1#l^x|`>9fp}io9zFr6eki zmtKx{uE{qD#^EoE#3y2G!jqJe!l$cwc`tjbrA**;YyQ^&amcqunO2$2)4wueTZG*r zMT9qGZ|!0u72D)SR=qOvx@St6hMTzdaQy^F?K=mR3G>d3WG|Vxv8jOX!mZtZ3=(M` zvq@1rx4hu0L0g$SNA-tmalv0GdQ7$j?24Oix)u%X{Fgkg#p+D;m)dG#dcZ!t)3wgUNGl zmJ;FaDxw$6Qpr0tr(f#&xq5`G%Q1*T9c5lC@+BE zPy*rnowVJNd3K5nRZPJM0|{TaT7T>>13DA$J<4Q8{@$AsA$UxC7-Zc7`Mu-(*>crQ z_2iyX_WX;JuQ8k$>}GTu+JMyRPDdVi2K#!%`Fi~mJ^d+ORTxKd9Fgv2SuQ*!pt;ROb+(of7Z?bU9GGcz_6U>YcUdN+{&w{iI|tjUW~JnDV&$&pq)j(ay>Iq|j2 z*sHX>r6{P!Qp>i_`)b&@ofq1QHM-A&#+oc08Ep|J7x_lihIfdeQF@r$iQ%7oQ`NsDh^WH zM;zzHzDfhHGCg{W>Dp|M6tnH9Ke~fmjKAE`d^NpzEPOm*-G{InQ!lhzSq#)*U8cX? zBkl;xM}VTK586brZccISw@-|FV+65`sNEszV<9gSAAP$E)T@#G(cEq49WhQhBA?3> zN~c$U!z?(|KqJ`w0a*^mAH{T*L$=3+04GZpU85MJjvBwA;-rQ$U$ycCz)s?^apu$? z#H!&1xUV6VAsEw6d3e=3252-{bcdE>I0`odQo2U( zKT$qiP&a3nelwCw7*?av3)o)U1NM%e?eB8Ii`W|>t|$v3M)tv$HuTuwpBnaMkbs%BM{XTp6Q>UzXb_8CXJ=_TOt~ARW*_^d;y3ALJd{9#NrsW zoeRE#*h?@g*afR6SC#8>PZN8_QX1=j`>H;>slDVr&;k-ovD+!fZeMN8>2^*pV~x_9 zY?qOBBv%_FH3NGh{LA=W=bGXndwDyc6(!xI?@tPPHa++-+~kw#q2y`FtjE^~i&r@l z;d1q20&%eM81Mz8%d=lA{7!Gg3mh$51Iu2)heO@;+T7o_M63yz;bI(SX|2Sx5S#7MuywtFmc)2hr(${Eq@$$ zfBd^_Mo&XR`==%jh_FfiFGat;*YJ-sma?vBLry6LnSnS14;qM@3X^MJY=(3Im8lZUbPs?GE#<-x1i#YF)*@) z*eJ^ny!7tnjO9OiPx4_^k(GfwT-8xXX#;0#n(T^ty1@9t*b3wL0%h( zULv}^bZ|iFO`o@b)R!FL-`c7oXO8;E=RozsqQ_k6J66Of-dwekda;AZ)ARlf?~JjF z?VI+!!R)UzGnh-@RhDV>9%I#*hP0;uc{orN$7ch3rE|;~?Ym7qxe1Lxe;(E7o8?&^ zg{S|@aN#G-2=-|)OdyUz&%?|d3Qc*K5@JFgb3oo@OJVG;_Zn^VcZHmB8l-7-?YJ=3 z6XnB2q3n}310Amm3OOj~qc99r$OZ_oMY5xRH13I$nr1GknR7V@wZMu6sCm?4tCp?) zOJ0gxr(&?*uF3uJURIq--(hB=%#Z*BQSS9U$*Henp+Xpb)WBEtwvnoN$PH{Feisi- z^l>}NnPkOtOq1H1%g^p}Pa+&eFlzl1p5(s95k9FsjvS0nJ!Yj-9wfK=Fkhsz-;ZSU zqsYE>bsXsL2B;Emvks<<@;DjUDQ~w7>mb!J+5a4h;(uv%j{_~_WJC1}MT?TIy>4_g zV!*e!xN5H$9sTAV+Z|z8c9t0`qcw2(H1VF4C*)Mngw0fzF;5yAxIC0OWB3Doy+0juJD6H`*cdFTF9K* zq4#G1wL-WS%%L!90Fn~r+pCGxNz;!pEt1QZl8=Zn{ujghF42Om)bLN^xr+FS2IA>Y zm_2AM4%Xpcf?L&A_GSp+o@nc*I_;=%d;cW}pr?Ygif;;9inOfUH=;tY;e zaQ$UyUmP*Q$saPwBKgC+E{s;Hr;uY{n=R`|!FXIIL1J)5 z2xOi-_q+`x9cngEhXVo7xXP6Q6UnzEP0x#5H~9RB%L)f=(5$wa2&mxBF50M$IWoN) znPJfK=bWHTXwKFzuU$_2NtXm73Ym17)k5!@tQCh@%?~`QaA>wqwb@q9ogHuuQ@VY^khV zdC+mV4z0E&KGzo?G2B`iM&9A!zAPhxjiiK;-camZYJ?tMs@Kl#m#0)VWo7feVOV*r zR1SM{%7%)FrLY4X3KT=q&)0s71uxTQpD{y5u-#p_9SBI>_`dkQRw_e}03~37b>}nR zJ$g~>S6}sWnM0*Mi?4-`!|}y%Bpvxu^f|Kmmt>Cv8!Twry;tf?ns(JstL4xrLE&`I z+0{!exx{ab9K3uUS^MA$QS!f8u4yjsph_iX3bp;wblKE5rjN@D*Rvo3XwC2R&R?7f zDE(M>x(9AC_B1vvUlo#MLwhsWdIdQ|f5cc4VQZ*TTgGsNo+G7G{+^8e)>v1o!py(M zbD1CFW90eesr+M}7n~mdY2s@)V+k&OfNrb8ip;(a2g+bk0%KI%5xZ0*Emev^&k z=_S6S`goRfjVV#O)aANrg}i6xKbo>a*P%hRwc#B8?^QQUxxW7Vf4ZPpO;>@O;j-| z?N7t$VKR!%^gs}K`r9&IH2h8RGb$}Zcji(W7+_|s)-SWGoQY<+h6zq>SX&6gM9wz( zB2?i1e!8U3cHYfV4S%@iGD(*7W73mz<5*3Nyj&r>-f#_Ct}h!@_-Q1_dyxXI&~pwd zYHFxFIJnMC+)Ki-)VpQeF|A0QFDmTMfcE2uUAeWhfc5G=9!ujus!h~On5e+j?ys*` z*9z6HoU6m1o*3_YP0==uWQJ&{zZl6X2lFJ(Yb&<)ZuabS8NbPPUcP)p^*h}nN-B;! z(MKa}MPiZJpuFD+`R=+Ek@Dh=L#|&w$l`mUvtTU@Z1dSoFp3w5_?R<^NlWyI!sn?G zMPl{#g;!%tY_X`|uzA(;eszoUBk9&`>TdGr_U|1f3QFcL|Gmlk{!vnP40Z@ z5Iye^Vw4^PmLc#1J>FwSo06VE%zRzac4%W@FNDb4Yn3h2F0<4>z26h+8&IKp^_h|I(V9n__y>uR&4{^=qM4@+FBXv;Pge(Pk~5E;2l^xcOjp zb8Vgo4%|Vkxh9kA1ME$zHV1Jx?5pFtSZ8%7&IXGhLu&2L6Bn*DwZElw_nM%G113v- zp-{rdHQZf-d+{3<=E$c*gcF}6F8Z>Zi64%cmX6*=#1sI683nc;4)E+d|QD{?Z`Lv?$*c&f8yW%Lutiwy)k<3tr48%uMZel z^=jvFJ+Ruq8kdEAmZ(G+=v9n*^$XMBjrn~3$+K8odwV5Hb7H`xlvO;Re(fONw!KYx zv{RM%tIqwvnAxIBA`FYr7v};yh1{U*4}Ceo76ic&Z@LVQ_bsyX#&OjsLP^A`_Za3E zjHXiI;xG%AtMmvoG` z|4TmSLqbe;x3+u0T+T@M<2MvIrj{bFCQ4^i>}s%dwh16D3sHo{_HWpeGsxrDOr0_H zU%oxlw_E0-6rynYBnYce?O14uZ{M|F6>F-hwBAUr& z;pER$G4Ed)4LoAx&638|_n)m?Ki#Itt7$oETtd7-#ow2kM&MDJ69M)KM=3Ct6ZyDK zttmH*#}B>}<`dW^Ya~-_=?i9Yl1pQo+G$fWB9De5mSWn9@T-M3Q6WYc=WEhaK5kXx z&u*aU8d>36wMj7N-zw77sirt_LG5pbY0DLQ_kodFYN9K?XfiBN=zRl3%hI(Q zU7q-$(`>u?zem;m5wRZcG+NUumBoHs-VcvqKx+eM<}j-ft}# z(@M~=mFJe33n7TLXLP8+jnxE7qL)x^Ce3ka5v)^}IofU;u3*Aw$8uV6I`;#8sQv3w z8bC&mNa;4#s=H4GFZTefMS2V{Dn{@avW!S-fEkkATMQ|PQ9l%kmRk?A(Wg3HXhphR zw1m+ z(75oDG-$gDx?74nS5f^bLTk6CKU|$k<`*yBk7i9f56P-3Os(mal@ifmj0q zCR5DSMqBc+4L5y@seF#x^|)%uOvUV{@nWQIE%q=#U>oO@(o_p*pn>lc{{~^>=NZ!%&#A@MEumi|R~-#lHkNVcSv&PwhaOh&D5_0Ia&@o%J~b%Q_;Ou3`$SoZVul-vi{fG@3G49k~na7KIs z{!BTz^r#GouThK>niSq$>WN+0x4|TKNBEux@@IA_nA0rW;(|Pslm>d96n6@*d}ok& z{IA#Xv^#1&zovt64OnFTO@VAMrP}3|HDxPpHKUhpLQ!m61pI!z_z#`Gg$Vjd8~H0y zu|3VjOg-7!Siq)x$mNgJ!{Y*342B{T7GpM(1^xS7d~XPNmVAtorHH;qogCSfKkA_I zgjNVr0@KD6aM!>UK7y)Z<(@o+e(8#nf*MPp347taUI#VYpWlAiZYD#q<{+36D<3cjgpg^YvNI!Eh4wc0A#9i&$j z7S0Ry+lPgg>{;**IDWmBxK_ADICFkjR*QY~eR2|>sy>{!bhS6AJN8w=vNT-9*gfXQ znNF$NR~frHHHtb}%y(b2>ghRGGz-377(^W&C`IrW&gFmH-#TI*%${c& z+8A`^7#gzXh&p$~g-5#fyH!ufgs(9Afo#iUVB$<`9;19YyhLb&2e8=?aByX3k~Tie zUg#G^*U+*ZqC1r0cdYr9-g5;dDlzJ2=7n1EnCMUrqviN9F^}FbAxzuwTd&*WB$pzRX6^3tCQgY-_=-Vq1q8)Xi| z;&?MrOXk22ujXMSKB|wuA2V&^^qqX9Js-ls39n3x%%L)taJHLp0Tn~AP-YmMug=nl z_OKj@Khm8GD{3OU@bdxr9VJ{Ql9Jxh12-MLm~okMKMrX6 ztXB&n^AjTRVa?Yz_&XUDU%%Fc|MQ+L-MINirtghd8d6wq$!6C2Ul|8Q!|(A$>toZc zuEZYJ>N32lEEgIxRX$ltT?ulu%*P=d#@ggq-Wv-^~a iqT=TN(;vC20&z;yU;L6tRGEGR{8W{+6e|@hLjMPD9Ny3X literal 0 HcmV?d00001 diff --git a/frontend/app/images/shame.jpg b/frontend/app/images/shame.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5ba59e8f5517b5ece76fd4c2281f2763847f1691 GIT binary patch literal 65156 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 DTes=7 literal 0 HcmV?d00001 diff --git a/frontend/app/images/youfail.png b/frontend/app/images/youfail.png new file mode 100644 index 0000000000000000000000000000000000000000..0f60a7ec7673e7ace0dd8594d0ccbaed363d560c GIT binary patch literal 8773 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

    EcoKnowledge

    +

    {{dashboard.user.username}} - {{dashboard.user.dashboard}}

    diff --git a/frontend/app/views/homepage/homepage-challenge.html b/frontend/app/views/homepage/homepage-challenge.html index 3facad3..3eaa286 100644 --- a/frontend/app/views/homepage/homepage-challenge.html +++ b/frontend/app/views/homepage/homepage-challenge.html @@ -1,5 +1,8 @@ -
    - +
    + + + +
    @@ -32,16 +35,31 @@
    - -
    + + + + + + + + + + + -
    + + + + + {{condition.description}} +
    +
    -
    {{condition.percentageAchieved | number}}% From cde7316645dc90b3bdc0132d33e4d798900f05c3 Mon Sep 17 00:00:00 2001 From: Benjamin Benni Date: Wed, 9 Sep 2015 17:21:57 +0200 Subject: [PATCH 28/28] Clean project for final release Delete unused frontend files and rename XXV2 into XX --- backend/src/challenge/TeamChallengeFactory.ts | 2 + backend/src/challenge/UserChallenge.ts | 2 + frontend/app/favicon.ico | Bin 4286 -> 797 bytes frontend/app/index.html | 12 +- frontend/app/scripts/app.js | 55 +----- .../{ServiceBadgeV2.js => ServiceBadge.js} | 14 +- .../app/scripts/controllers/ServiceSensor.js | 23 --- frontend/app/scripts/controllers/about.js | 17 -- frontend/app/scripts/controllers/badge.js | 118 ++----------- frontend/app/scripts/controllers/badgeV2.js | 28 ---- frontend/app/scripts/controllers/goal.js | 2 +- frontend/app/scripts/controllers/home.js | 158 ------------------ frontend/app/scripts/controllers/main.js | 17 -- frontend/app/scripts/controllers/viewGoal.js | 34 ---- frontend/app/views/create-badge-perso.html | 23 --- frontend/app/views/create-badge.html | 50 +----- frontend/app/views/login.html | 3 +- 17 files changed, 37 insertions(+), 521 deletions(-) rename frontend/app/scripts/controllers/{ServiceBadgeV2.js => ServiceBadge.js} (64%) delete mode 100644 frontend/app/scripts/controllers/ServiceSensor.js delete mode 100644 frontend/app/scripts/controllers/about.js delete mode 100644 frontend/app/scripts/controllers/badgeV2.js delete mode 100644 frontend/app/scripts/controllers/home.js delete mode 100644 frontend/app/scripts/controllers/main.js delete mode 100644 frontend/app/scripts/controllers/viewGoal.js delete mode 100644 frontend/app/views/create-badge-perso.html diff --git a/backend/src/challenge/TeamChallengeFactory.ts b/backend/src/challenge/TeamChallengeFactory.ts index c9ffe0a..3a914bf 100644 --- a/backend/src/challenge/TeamChallengeFactory.ts +++ b/backend/src/challenge/TeamChallengeFactory.ts @@ -45,6 +45,8 @@ class TeamChallengeFactory { for(var currentChildIDIndex in childrenIDs) { var currentChildID = childrenIDs[currentChildIDIndex]; var currentChild = userChallengeRepository.getChallengeByID(currentChildID); + currentChild.setTakenBy(team.getName()); + children.push(currentChild); } diff --git a/backend/src/challenge/UserChallenge.ts b/backend/src/challenge/UserChallenge.ts index 611af8e..144b592 100644 --- a/backend/src/challenge/UserChallenge.ts +++ b/backend/src/challenge/UserChallenge.ts @@ -332,6 +332,8 @@ class UserChallenge { } + setTakenBy(teamName:string) {this.takenBy = teamName;} + getStatusAsString():string { switch (this.status) { case 0: diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico index 6527905307f19ba00762f9241f7eb535fa84a2f9..ea326057eb47759ce3449c324ff393e5ad16c1ae 100644 GIT binary patch literal 797 zcmV+&1LFLNP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02p*dSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E;js(W8VM(0**;UK~zXf?Ue0nPGKC!{Q(s6f~3gUhFF_0 zkF9yCrL44xlBUcz+*e>gMJZfWx7|S=lwz_Z_N}pxnkxUUO5ZC z@zrk*{X?5sEwh4B-|tz2nT%5{o2gDm`A)a|JKS*EBagJ2HAeG^A^2m1D;|ULK5)ExaGa~_? z>1v|RuN9Dci58t%P1IMiJDt%o{UhWPA5Kx@H@DI($4Ww5&M$3XWe2Y#0sAS@$ECTd z*jq#SaSos4Xah&IblTljl&L{sHpKyUs8em~QA0KR8uP>*+d!Qdtv&&mnKlvE_T-+SC z*QNCVwcvq%+&DDc+T}Uf(2_FavDN{-&hCpIs?aW=A$mcrzyD+9(025i1~K&uVf&w4 zItQLK9T{7k?s@bnU*&p+<^UI*aHA1aH+Fo^PAzM|xjNK09?2V(Cme7IFB(BP?7#at z(>DB3w`AUFS~=(LUBdZ>v-SG4J~%Mrfj&05Z)oj13l5tbEq4x>8+;FC0Dvr zbJY#7PS$+yE_Cf7gxqQEC@RoZX5J^}71l+`Q~qnOF4D za`lhjUuqZa-sj)EHDleV2i|mc!Ly-@7IwzPM{?pBUt(+@IHi8HTz#Iq9)9h|hrL3) zfOT#@|5$JCxmRjsOj>&kUt(m8*57|W(FoE`CX*8edYv%j=3sR5>!hvglJ#@8K6j$g z&IuUbRC_{)p}sbyx%UD6Fki;t6nDk0gT5&6Q_at7FbVVOu?4VK{oR#!kyYbCc;<4+LITzoZ8-~O5L+9MiLHL4NyME>! z;Ky7<)UR!gN_~GXhMvPMHNB;EmmIK}eHD&~cRx89jth}IM#tU%ablw0|GxfE9IjRR zl-)b-IvC#UD!IewzPL77SI>R+?}<2ERr|R2o~zCC8rJUR8>DI5*0O$6+k~wZ)Mt;b z(Hul-OFl+F))}lK&&Yi*+S2kJmHDbdBWOQnaSA6S|#*
    @@ -68,8 +68,8 @@

    ecoknowledge - a SmartCampus project

    ---> +--> - - - - - - - + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index db6fe31..dbb5c3f 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -29,76 +29,37 @@ var app = angular .when('/dashboard/view/', { templateUrl: '../views/dashboard.html', controller: 'DashboardCtrl', - controllerAs:'dashboard' + controllerAs: 'dashboard' }) .when('/dashboard/view/:id/', { templateUrl: '../views/dashboard.html', controller: 'DashboardCtrl', - controllerAs:'dashboard' + controllerAs: 'dashboard' }) .when('/dashboard/view/:id/:dashboardType', { templateUrl: '../views/dashboard.html', controller: 'DashboardCtrl', - controllerAs:'dashboard' + controllerAs: 'dashboard' }) .when('/create-goal', { templateUrl: '../views/create-goal.html', controller: 'GoalCtrl' }) - .otherwise({ - redirectTo: '/lolerreurdansredirectionangulareuuuh/' - }); - - /* - $routeProvider - .when('/', { - templateUrl: '../views/homepage/homepage.html', - controller: 'HomeCtrl', - controllerAs: 'homeCtrl' - }) - .when('/about', { - templateUrl: 'views/about.html', - controller: 'AboutCtrl' - }) - .when('/create-goal', { - templateUrl: 'views/create-goal.html', - controller: 'GoalCtrl' - }) .when('/create-badge', { - templateUrl: 'views/create-badge.html', + templateUrl: '../views/create-badge.html', controller: 'BadgeCtrl', - controllerAs: 'badgeCreateCtrl' - }) - .when('/view-goal/:goalId', { - templateUrl: 'views/view-goal.html', - controller: 'ViewGoalCtrl', - controllerAs: 'viewGoalCtrl' - }) - .when('/create-badge-perso', { - templateUrl: 'views/create-badge-perso.html', - controller: 'BadgeCtrlV2' - }) - .when('/dashboard', { - templateUrl: '../views/dashboard.html', - controller: 'DashboardCtrl', - controllerAs:'dashboard' - }) - .when('/login', { - templateUrl: '../views/login.html', - controller: 'LoginCtrl', - controllerAs:'loginCtrl' + controllerAs: 'badgeCtrl' }) .otherwise({ redirectTo: '/' }); - */ }); -app.filter('range', function() { - return function(input, total) { +app.filter('range', function () { + return function (input, total) { total = parseInt(total); - for (var i=0; i - - -
    -
    - - -
    - -
    - Points à gagner - - {{badgeCreateCtrl.badge.points}} -
    - -
    - -
    -
    - \ No newline at end of file diff --git a/frontend/app/views/create-badge.html b/frontend/app/views/create-badge.html index 56b896f..a21b471 100644 --- a/frontend/app/views/create-badge.html +++ b/frontend/app/views/create-badge.html @@ -1,4 +1,4 @@ -
    +
    @@ -10,60 +10,12 @@

    Créer un badge

    ng-model="badgeCreateCtrl.badge.name" required>
    -
    - -