<commit_before>'use strict';

/**
 * Created by:    Robin Hansma (robin@ihatestatistics.com)
 * Date:          5-5-2015
 */

/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
    paginate = require('express-paginate'),
    errorHandler = require('./errors.server.controller'),
    Goal = mongoose.model('Goal'),
    UserGoals = mongoose.model('UserGoals'),
    _ = require('lodash'),
    mail = require('./mail.server.controller.js'),
    tincan = require('./tincan.server.controller.js');

/**
 * Create a goal
 */
exports.create = function(req, res) {
  var goal = new Goal(req.body);
  goal.creator = req.user;
  goal.finished = false;
  goal.committed = 1;

  goal.save(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      var requestUrl = req.protocol + '://' + req.get('host') + req.url;
      tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_CREATED, requestUrl, 'Goal');

      var userGoals = new UserGoals();
      userGoals.status = 'committed';
      userGoals.user = req.user;
      userGoals.goal = goal._id;

      userGoals.save(function(err) {
        if (err) {
          return res.status(400).send({
            message: errorHandler.getErrorMessage(err)
          });
        } else {
          tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_COMMITTED, requestUrl, 'Goal');
          res.json(goal);
        }
      });
    }
  });
};

/**
 * Show the current goal
 */
exports.read = function(req, res) {
  res.json(req.goal);
};

/**
 * Base method for updating, used for both update methods, so normal update can filter
 * @param req
 * @param res
 * @private
 */
function _update(req, res, goal) {
  goal.save(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      var requestUrl = req.protocol + '://' + req.get('host') + req.url;

      tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_UPDATED, requestUrl, 'Goal');
      res.json(goal);
    }
  });
}

/**
 * Update a goal
 */
exports.update = function(req, res) {
  var goal = req.goal;

  /* Omit rating property, may only be changed by teacher */
  goal = _.extend(goal, _.omit(req.body, ['rating', 'committed', 'finished']));
  _update(req, res, goal);
};

/**
 * Update by teacher, for also updating rating
 * @param req
 * @param res
 */
exports.updateByTeacher = function(req, res) {
  var goal = req.goal;

  goal = _.extend(goal, req.body);

  /* Teacher rates goal, send statement to LRS */
  if(goal.rating !== null) {
    var requestUrl = req.protocol + '://' + req.get('host') + req.url;
    tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_RATED, requestUrl, 'Goal', goal.rating);
  }
  _update(req, res, goal);
};

/**
 * Delete an goal
 */
exports.delete = function(req, res) {
  var goal = req.goal;

  goal.remove(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      UserGoals.remove({'goal': goal._id}).exec(function(err) {
        if(err) {
          return res.status(400).send({
            message: errorHandler.getErrorMessage(err)
          });
        } else {
          res.json(goal);
        }
      });
    }
  });
};

/**
 * List of Goals
 */
exports.list = function(req, res) {
  console.log('test');
  console.log(req);
  Goal.paginate({'private': {$ne: true}}, req.query.page, req.query.limit, function(err, pageCount, results) {
    if (err) {
      console.log(err);
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      console.log('no err');
      res.format({
        json: function() {
          res.json([
            paginate.hasNextPages(req)(pageCount),
            results
          ]);
        }
      });
    }
  });
};

/**
 * Find and return all approved goals which are not approved or rejected by the user
 * @param req
 * @param res
 */
exports.approved = function(req, res) {
  /* Find all goals committed or rejected */
  UserGoals.find({'user': req.user}, {goal: 1}).exec(function(err, usergoals) {
    var goals = [];
    for(var i in usergoals){
      goals.push(usergoals[i].goal);
    }

    /* Find and return all goals not already committed or rejected */
    Goal.find({_id: {$nin: goals}}).where('rating').gt(5).exec(function(err, goals) {
      if (err) {
        return res.status(400).send({
          message: errorHandler.getErrorMessage(err)
        });
      } else {
        var requestUrl = req.protocol + '://' + req.get('host') + req.url;
        tincan.sendStatementOnUser(req.user.email, process.env.TINCAN_VIEW, requestUrl, 'inspirational goal list');
        res.json(goals);
      }
    });
  });
};

/**
 * Publish all goals, model checks if rating is above 5.5, so not necessary to check here
 * @param req
 * @param res
 */
exports.publish = function(req, res) {
  Goal.find().exec(function(err, goals) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      _.each(goals, function(goal, index, list) {
        goal.published = true;
        goal.save();

        var requestUrl = req.protocol + '://' + req.get('host') + req.url;
        tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_APPROVED, requestUrl, 'Goal');

        /* If this was the last goal, send mail and respond with 200 */
        if(index === (_.size(list) - 1)) {
          /* Send mail to students */
          var subject = 'Goals are published';
          var body = '
The goals are published

It is now possible to commit to goals, ' +
            'reject goals and report on the status of your goals. Follow this link (' + process.env.APP_URL + ') to commit to goals.
';
          mail.mailToAll(subject, body);

          res.json(goals);
        }
      });
    }
  });
};

/**
 * Goal middleware
 */
exports.goalByID = function(req, res, next, id) {
  Goal.findById(id).populate('creator', 'displayName').exec(function(err, goal) {
    if (err) return next(err);
    if (!goal) return next(new Error('Failed to load goal ' + id));
    req.goal = goal;
    next();
  });
};

exports.paginate = function(req) {
  //req.query.page = req.params.page;
};

/**
 * Goal authorization middleware
 */
exports.hasAuthorization = function(req, res, next) {
  if (req.goal.creator.id !== req.user.id) {
    return res.status(403).send({
      message: 'User is not authorized'
    });
  }
  next();
};<commit_msg>Removed unused function<commit_after>'use strict';

/**
 * Created by:    Robin Hansma (robin@ihatestatistics.com)
 * Date:          5-5-2015
 */

/**
 * Module dependencies.
 */
var mongoose = require('mongoose'),
    paginate = require('express-paginate'),
    errorHandler = require('./errors.server.controller'),
    Goal = mongoose.model('Goal'),
    UserGoals = mongoose.model('UserGoals'),
    _ = require('lodash'),
    mail = require('./mail.server.controller.js'),
    tincan = require('./tincan.server.controller.js');

/**
 * Create a goal
 */
exports.create = function(req, res) {
  var goal = new Goal(req.body);
  goal.creator = req.user;
  goal.finished = false;
  goal.committed = 1;

  goal.save(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      var requestUrl = req.protocol + '://' + req.get('host') + req.url;
      tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_CREATED, requestUrl, 'Goal');

      var userGoals = new UserGoals();
      userGoals.status = 'committed';
      userGoals.user = req.user;
      userGoals.goal = goal._id;

      userGoals.save(function(err) {
        if (err) {
          return res.status(400).send({
            message: errorHandler.getErrorMessage(err)
          });
        } else {
          tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_COMMITTED, requestUrl, 'Goal');
          res.json(goal);
        }
      });
    }
  });
};

/**
 * Show the current goal
 */
exports.read = function(req, res) {
  res.json(req.goal);
};

/**
 * Base method for updating, used for both update methods, so normal update can filter
 * @param req
 * @param res
 * @private
 */
function _update(req, res, goal) {
  goal.save(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      var requestUrl = req.protocol + '://' + req.get('host') + req.url;

      tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_UPDATED, requestUrl, 'Goal');
      res.json(goal);
    }
  });
}

/**
 * Update a goal
 */
exports.update = function(req, res) {
  var goal = req.goal;

  /* Omit rating property, may only be changed by teacher */
  goal = _.extend(goal, _.omit(req.body, ['rating', 'committed', 'finished']));
  _update(req, res, goal);
};

/**
 * Update by teacher, for also updating rating
 * @param req
 * @param res
 */
exports.updateByTeacher = function(req, res) {
  var goal = req.goal;

  goal = _.extend(goal, req.body);

  /* Teacher rates goal, send statement to LRS */
  if(goal.rating !== null) {
    var requestUrl = req.protocol + '://' + req.get('host') + req.url;
    tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_RATED, requestUrl, 'Goal', goal.rating);
  }
  _update(req, res, goal);
};

/**
 * Delete an goal
 */
exports.delete = function(req, res) {
  var goal = req.goal;

  goal.remove(function(err) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      UserGoals.remove({'goal': goal._id}).exec(function(err) {
        if(err) {
          return res.status(400).send({
            message: errorHandler.getErrorMessage(err)
          });
        } else {
          res.json(goal);
        }
      });
    }
  });
};

/**
 * List of Goals
 */
exports.list = function(req, res) {
  console.log('test');
  console.log(req);
  Goal.paginate({'private': {$ne: true}}, req.query.page, req.query.limit, function(err, pageCount, results) {
    if (err) {
      console.log(err);
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      console.log('no err');
      res.format({
        json: function() {
          res.json([
            paginate.hasNextPages(req)(pageCount),
            results
          ]);
        }
      });
    }
  });
};

/**
 * Find and return all approved goals which are not approved or rejected by the user
 * @param req
 * @param res
 */
exports.approved = function(req, res) {
  /* Find all goals committed or rejected */
  UserGoals.find({'user': req.user}, {goal: 1}).exec(function(err, usergoals) {
    var goals = [];
    for(var i in usergoals){
      goals.push(usergoals[i].goal);
    }

    /* Find and return all goals not already committed or rejected */
    Goal.find({_id: {$nin: goals}}).where('rating').gt(5).exec(function(err, goals) {
      if (err) {
        return res.status(400).send({
          message: errorHandler.getErrorMessage(err)
        });
      } else {
        var requestUrl = req.protocol + '://' + req.get('host') + req.url;
        tincan.sendStatementOnUser(req.user.email, process.env.TINCAN_VIEW, requestUrl, 'inspirational goal list');
        res.json(goals);
      }
    });
  });
};

/**
 * Publish all goals, model checks if rating is above 5.5, so not necessary to check here
 * @param req
 * @param res
 */
exports.publish = function(req, res) {
  Goal.find().exec(function(err, goals) {
    if (err) {
      return res.status(400).send({
        message: errorHandler.getErrorMessage(err)
      });
    } else {
      _.each(goals, function(goal, index, list) {
        goal.published = true;
        goal.save();

        var requestUrl = req.protocol + '://' + req.get('host') + req.url;
        tincan.sendStatementOnGoal(req.user.email, goal._id, process.env.TINCAN_APPROVED, requestUrl, 'Goal');

        /* If this was the last goal, send mail and respond with 200 */
        if(index === (_.size(list) - 1)) {
          /* Send mail to students */
          var subject = 'Goals are published';
          var body = '
The goals are published

It is now possible to commit to goals, ' +
            'reject goals and report on the status of your goals. Follow this link (' + process.env.APP_URL + ') to commit to goals.
';
          mail.mailToAll(subject, body);

          res.json(goals);
        }
      });
    }
  });
};

/**
 * Goal middleware
 */
exports.goalByID = function(req, res, next, id) {
  Goal.findById(id).populate('creator', 'displayName').exec(function(err, goal) {
    if (err) return next(err);
    if (!goal) return next(new Error('Failed to load goal ' + id));
    req.goal = goal;
    next();
  });
};

/**
 * Goal authorization middleware
 */
exports.hasAuthorization = function(req, res, next) {
  if (req.goal.creator.id !== req.user.id) {
    return res.status(403).send({
      message: 'User is not authorized'
    });
  }
  next();
};<|endoftext|>


<commit_before>// test wiki api:

var fs = require('fs');

var
    _ = require('lodash'),
    async=require('async'),
    should = require('should');

var remote = require('./_test');

var log = console.log;

describe('#wikis', function() {

    before(remote.setup);

    describe('#getwikis', function() {

        it('should get empty wikis', function(done) {
            remote.get(remote.guest, '/api/wikis', null, function(wikis) {
                wikis.length.should.equal(0);
                done();
            });
        });

        it('create and update wiki by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: 'Test Wiki   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: '  Long content... '
            }, function(r1) {
                r1.name.should.equal('Test Wiki');
                r1.description.should.equal('blablabla\nhaha...');
                r1.tags.should.equal('aaa,BBB,ccc');
                r1.content.should.equal('Long content...');
                r1.cover_id.should.equal('');

                // update wiki:
                remote.post(remote.editor, '/api/wikis/' + r1.id, {
                    name: 'Name Changed  ',
                    content: 'Changed!'
                }, function(r2) {
                    r2.name.should.equal('Name Changed');
                    r2.content.should.equal('Changed!');
                    // query:
                    remote.get(remote.guest, '/api/wikis/' + r1.id, null, function(r3) {
                        r3.name.should.equal(r2.name);
                        r3.content.should.equal(r2.content);
                        // not updated:
                        r3.tags.should.equal(r1.tags);
                        r3.description.should.equal(r1.description);
                        done();
                    });
                });
            });
        });

        it('create wiki with cover by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' Test Wiki With Cover  ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' cover,\n CoveR',
                content: '  Wiki comes with cover...   ',
                file: remote.createReadStream('./test/res-image.jpg')
            }, function(r) {
                r.name.should.equal('Test Wiki With Cover');
                r.description.should.equal('blablabla\nhaha...');
                r.tags.should.equal('cover');
                r.content.should.equal('Wiki comes with cover...');
                r.cover_id.should.be.ok;
                // check cover:
                remote.get(remote.guest, '/api/attachments/' + r.cover_id, null, function(r2) {
                    r2.id.should.equal(r.cover_id);
                    r2.name.should.equal(r.name);
                    r2.size.should.equal(346158);
                    // download image:
                    remote.download('/files/attachments/' + r2.id, function(content_type, content_length, content) {
                        content_type.should.equal('image/jpeg');
                        content_length.should.equal(346158);
                        // update cover:
                        remote.post(remote.editor, '/api/wikis/' + r.id, {
                            name: 'Cover changed!',
                            file: remote.createReadStream('./test/res-image-2.jpg')
                        }, function(r3) {
                            // check cover is ok:
                            r3.cover_id.should.not.equal(r.cover_id);
                            remote.get(remote.guest, '/api/attachments/' + r3.cover_id, null, function(r4) {
                                r4.id.should.equal(r3.cover_id);
                                // check article cover changed:
                                remote.get(remote.guest, '/api/wikis/' + r.id, null, function(r5) {
                                    r5.cover_id.should.equal(r4.id);
                                    done();
                                });
                            });
                        });
                    });
                });
            });
        });

        it('create wiki with wrong parameter by editor', function(done) {
            var create_missing_params = function(pname) {
                var r = {
                    name: 'Test',
                    description: 'blablabla...',
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...'
                };
                delete r[pname];
                return r;
            };
            var tests = _.map(['name', 'description', 'content'], function(param) {
                return function(callback) {
                    remote.post(remote.editor, '/api/wikis', create_missing_params(param), function(r) {
                        r.error.should.equal('parameter:invalid');
                        r.data.should.equal(param);
                        r.message.should.be.ok;
                        callback(null);
                    });
                };
            });
            async.series(tests, function(err, results) {
                done();
            });
        });

        it('create by contributor', function(done) {
            // create wiki:
            remote.post(remote.contributor, '/api/wikis', {
                name: ' To be delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(r) {
                should(r.error).be.ok;
                r.error.should.equal('permission:denied');
                done();
            });
        });

        it('create and delete wiki by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' To be delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(r1) {
                r1.name.should.equal('To be delete...');
                // delete article:
                remote.post(remote.editor, '/api/wikis/' + r1.id + '/delete', null, function(r2) {
                    r2.id.should.equal(r1.id);
                    // query:
                    remote.get(remote.guest, '/api/wikis/' + r1.id, null, function(r3) {
                        r3.error.should.equal('resource:notfound');
                        r3.data.should.equal('Wiki');
                        done();
                    });
                });
            });
        });

        it('create wikipage and try delete wiki', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' Tree   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(w1) {
                // create wiki page:
                // w1
                // +- p1
                remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                    parent_id: 'ROOT',
                    name: ' P1 - First Wiki Page   ',
                    content: ' This is a first wiki page...   '
                }, function(p1) {
                    should(p1.error).not.be.ok;
                    p1.wiki_id.should.equal(w1.id);
                    p1.parent_id.should.equal('');
                    p1.display_order.should.equal(0);
                    p1.name.should.equal('P1 - First Wiki Page');
                    p1.content.should.equal('This is a first wiki page...');
                    // try delete wiki:
                    remote.post(remote.editor, '/api/wikis/' + w1.id + '/delete', {}, function(r2) {
                        should(r2.error).be.ok;
                        r2.error.should.equal('resource:conflict');
                        // try create wiki page again:
                        // w1
                        // +- p1
                        //    +- p2
                        remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                            parent_id: p1.id,
                            name: 'P2',
                            content: 'child wiki page...\n\n'
                        }, function(p2) {
                            should(p2.error).not.be.ok;
                            p2.wiki_id.should.equal(w1.id);
                            p2.parent_id.should.equal(p1.id);
                            p2.name.should.equal('P2');
                            p2.content.should.equal('child wiki page...');
                            // try create wiki page under w1:
                            // w1
                            // +- p1
                            // |  +- p2
                            // +- p3
                            remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                                parent_id: 'ROOT',
                                name: 'P3',
                                content: 'p3'
                            }, function(p3) {
                                should(p3.error).not.be.ok;
                                p3.wiki_id.should.equal(w1.id);
                                p3.parent_id.should.equal('');
                                p3.display_order.should.equal(1);
                                p3.name.should.equal('P3');
                                p3.content.should.equal('p3');
                                // try create wiki page under p2:
                                // w1
                                // +- p1
                                // |  +- p2
                                // |     +- p4
                                // +- p3
                                remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                                    parent_id: p2.id,
                                    name: 'P4',
                                    content: 'p4'
                                }, function(p4) {
                                    should(p4.error).not.be.ok;
                                    p4.wiki_id.should.equal(w1.id);
                                    p4.parent_id.should.equal(p2.id);
                                    p4.display_order.should.equal(0);
                                    p4.name.should.equal('P4');
                                    p4.content.should.equal('p4');
                                    // move p3 to p2 at index 0:
                                    // w1
                                    // +- p1
                                    //    +- p2
                                    //       +- p3 <----- move to here
                                    //       +- p4
                                    remote.post(remote.editor, '/api/wikis/wikipages/' + p3.id + '/move/' + p2.id, {
                                        index: 0
                                    }, function(np3) {
                                        should(np3.error).not.be.ok;
                                        np3.wiki_id.should.equal(w1.id);
                                        np3.parent_id.should.equal(p2.id);
                                        np3.display_order.should.equal(0);
                                        // move p4 to ROOT at index 0:
                                        // w1
                                        // +- p4 <----- move to here
                                        // +- p1
                                        //    +- p2
                                        //       +- p3
                                        remote.post(remote.editor, '/api/wikis/wikipages/' + p4.id + '/move/ROOT', {
                                            index: 0
                                        }, function(np4) {
                                            should(np4.error).not.be.ok;
                                            np4.wiki_id.should.equal(w1.id);
                                            np4.parent_id.should.equal('');
                                            np4.display_order.should.equal(0);
                                            // move p1 to p3 to make a recursive:
                                            // w1
                                            // +- p4
                                            // +- p1       <----- i'm to here
                                            //    +- p2
                                            //       +- p3
                                            //          +- <----- to here, but not allowed!
                                            remote.post(remote.editor, '/api/wikis/wikipages/' + p1.id + '/move/' + p3.id, {
                                                index: 0
                                            }, function(r4) {
                                                should(r4.error).be.ok;
                                                r4.error.should.equal('resource:conflict');
                                                done();
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});
<commit_msg>fix test case of wikis<commit_after>// test wiki api:

var fs = require('fs');

var
    _ = require('lodash'),
    async=require('async'),
    should = require('should');

var remote = require('./_test');

var log = console.log;

describe('#wikis', function() {

    before(remote.setup);

    describe('#getwikis', function() {

        it('should get empty wikis', function(done) {
            remote.get(remote.guest, '/api/wikis', null, function(r) {
                should(r).be.ok;
                should(r.wikis).be.ok;
                r.wikis.should.be.instanceof(Array).and.have.lengthOf(0);
                done();
            });
        });

        it('create and update wiki by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: 'Test Wiki   ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' aaa,\n BBB,  \t ccc,CcC',
                content: '  Long content... '
            }, function(r1) {
                r1.name.should.equal('Test Wiki');
                r1.description.should.equal('blablabla\nhaha...');
                r1.tags.should.equal('aaa,BBB,ccc');
                r1.content.should.equal('Long content...');
                r1.cover_id.should.equal('');

                // update wiki:
                remote.post(remote.editor, '/api/wikis/' + r1.id, {
                    name: 'Name Changed  ',
                    content: 'Changed!'
                }, function(r2) {
                    r2.name.should.equal('Name Changed');
                    r2.content.should.equal('Changed!');
                    // query:
                    remote.get(remote.guest, '/api/wikis/' + r1.id, null, function(r3) {
                        r3.name.should.equal(r2.name);
                        r3.content.should.equal(r2.content);
                        // not updated:
                        r3.tags.should.equal(r1.tags);
                        r3.description.should.equal(r1.description);
                        // query all wikis:
                        remote.get(remote.guest, '/api/wikis/', null, function(r4) {
                            should(r4).be.ok;
                            should(r4.wikis).be.ok;
                            r4.wikis.should.be.instanceof(Array).and.have.lengthOf(1);
                            var w = r4.wikis[0];
                            w.name.should.equal(r3.name);
                            done();
                        });
                    });
                });
            });
        });

        it('create wiki with cover by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' Test Wiki With Cover  ',
                description: '   blablabla\nhaha...  \n   ',
                tags: ' cover,\n CoveR',
                content: '  Wiki comes with cover...   ',
                file: remote.createReadStream('./test/res-image.jpg')
            }, function(r) {
                r.name.should.equal('Test Wiki With Cover');
                r.description.should.equal('blablabla\nhaha...');
                r.tags.should.equal('cover');
                r.content.should.equal('Wiki comes with cover...');
                r.cover_id.should.be.ok;
                // check cover:
                remote.get(remote.guest, '/api/attachments/' + r.cover_id, null, function(r2) {
                    r2.id.should.equal(r.cover_id);
                    r2.name.should.equal(r.name);
                    r2.size.should.equal(346158);
                    // download image:
                    remote.download('/files/attachments/' + r2.id, function(content_type, content_length, content) {
                        content_type.should.equal('image/jpeg');
                        content_length.should.equal(346158);
                        // update cover:
                        remote.post(remote.editor, '/api/wikis/' + r.id, {
                            name: 'Cover changed!',
                            file: remote.createReadStream('./test/res-image-2.jpg')
                        }, function(r3) {
                            // check cover is ok:
                            r3.cover_id.should.not.equal(r.cover_id);
                            remote.get(remote.guest, '/api/attachments/' + r3.cover_id, null, function(r4) {
                                r4.id.should.equal(r3.cover_id);
                                // check article cover changed:
                                remote.get(remote.guest, '/api/wikis/' + r.id, null, function(r5) {
                                    r5.cover_id.should.equal(r4.id);
                                    done();
                                });
                            });
                        });
                    });
                });
            });
        });

        it('create wiki with wrong parameter by editor', function(done) {
            var create_missing_params = function(pname) {
                var r = {
                    name: 'Test',
                    description: 'blablabla...',
                    tags: 'tag1,tag2,tag3',
                    content: 'a long content...'
                };
                delete r[pname];
                return r;
            };
            var tests = _.map(['name', 'description', 'content'], function(param) {
                return function(callback) {
                    remote.post(remote.editor, '/api/wikis', create_missing_params(param), function(r) {
                        r.error.should.equal('parameter:invalid');
                        r.data.should.equal(param);
                        r.message.should.be.ok;
                        callback(null);
                    });
                };
            });
            async.series(tests, function(err, results) {
                done();
            });
        });

        it('create by contributor', function(done) {
            // create wiki:
            remote.post(remote.contributor, '/api/wikis', {
                name: ' To be delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(r) {
                should(r.error).be.ok;
                r.error.should.equal('permission:denied');
                done();
            });
        });

        it('create and delete wiki by editor', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' To be delete...   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(r1) {
                r1.name.should.equal('To be delete...');
                // delete article:
                remote.post(remote.editor, '/api/wikis/' + r1.id + '/delete', null, function(r2) {
                    r2.id.should.equal(r1.id);
                    // query:
                    remote.get(remote.guest, '/api/wikis/' + r1.id, null, function(r3) {
                        r3.error.should.equal('resource:notfound');
                        r3.data.should.equal('Wiki');
                        done();
                    });
                });
            });
        });

        it('create wikipage and try delete wiki', function(done) {
            // create wiki:
            remote.post(remote.editor, '/api/wikis', {
                name: ' Tree   ',
                description: '   blablabla\nhaha...  \n   ',
                content: '  Long long long content... '
            }, function(w1) {
                // create wiki page:
                // w1
                // +- p1
                remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                    parent_id: 'ROOT',
                    name: ' P1 - First Wiki Page   ',
                    content: ' This is a first wiki page...   '
                }, function(p1) {
                    should(p1.error).not.be.ok;
                    p1.wiki_id.should.equal(w1.id);
                    p1.parent_id.should.equal('');
                    p1.display_order.should.equal(0);
                    p1.name.should.equal('P1 - First Wiki Page');
                    p1.content.should.equal('This is a first wiki page...');
                    // try delete wiki:
                    remote.post(remote.editor, '/api/wikis/' + w1.id + '/delete', {}, function(r2) {
                        should(r2.error).be.ok;
                        r2.error.should.equal('resource:conflict');
                        // try create wiki page again:
                        // w1
                        // +- p1
                        //    +- p2
                        remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                            parent_id: p1.id,
                            name: 'P2',
                            content: 'child wiki page...\n\n'
                        }, function(p2) {
                            should(p2.error).not.be.ok;
                            p2.wiki_id.should.equal(w1.id);
                            p2.parent_id.should.equal(p1.id);
                            p2.name.should.equal('P2');
                            p2.content.should.equal('child wiki page...');
                            // try create wiki page under w1:
                            // w1
                            // +- p1
                            // |  +- p2
                            // +- p3
                            remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                                parent_id: 'ROOT',
                                name: 'P3',
                                content: 'p3'
                            }, function(p3) {
                                should(p3.error).not.be.ok;
                                p3.wiki_id.should.equal(w1.id);
                                p3.parent_id.should.equal('');
                                p3.display_order.should.equal(1);
                                p3.name.should.equal('P3');
                                p3.content.should.equal('p3');
                                // try create wiki page under p2:
                                // w1
                                // +- p1
                                // |  +- p2
                                // |     +- p4
                                // +- p3
                                remote.post(remote.editor, '/api/wikis/' + w1.id + '/wikipages', {
                                    parent_id: p2.id,
                                    name: 'P4',
                                    content: 'p4'
                                }, function(p4) {
                                    should(p4.error).not.be.ok;
                                    p4.wiki_id.should.equal(w1.id);
                                    p4.parent_id.should.equal(p2.id);
                                    p4.display_order.should.equal(0);
                                    p4.name.should.equal('P4');
                                    p4.content.should.equal('p4');
                                    // move p3 to p2 at index 0:
                                    // w1
                                    // +- p1
                                    //    +- p2
                                    //       +- p3 <----- move to here
                                    //       +- p4
                                    remote.post(remote.editor, '/api/wikis/wikipages/' + p3.id + '/move/' + p2.id, {
                                        index: 0
                                    }, function(np3) {
                                        should(np3.error).not.be.ok;
                                        np3.wiki_id.should.equal(w1.id);
                                        np3.parent_id.should.equal(p2.id);
                                        np3.display_order.should.equal(0);
                                        // move p4 to ROOT at index 0:
                                        // w1
                                        // +- p4 <----- move to here
                                        // +- p1
                                        //    +- p2
                                        //       +- p3
                                        remote.post(remote.editor, '/api/wikis/wikipages/' + p4.id + '/move/ROOT', {
                                            index: 0
                                        }, function(np4) {
                                            should(np4.error).not.be.ok;
                                            np4.wiki_id.should.equal(w1.id);
                                            np4.parent_id.should.equal('');
                                            np4.display_order.should.equal(0);
                                            // move p1 to p3 to make a recursive:
                                            // w1
                                            // +- p4
                                            // +- p1       <----- i'm to here
                                            //    +- p2
                                            //       +- p3
                                            //          +- <----- to here, but not allowed!
                                            remote.post(remote.editor, '/api/wikis/wikipages/' + p1.id + '/move/' + p3.id, {
                                                index: 0
                                            }, function(r4) {
                                                should(r4.error).be.ok;
                                                r4.error.should.equal('resource:conflict');
                                                done();
                                            });
                                        });
                                    });
                                });
                            });
                        });
                    });
                });
            });
        });
    });
});
<|endoftext|>


<commit_before>
import { COMPONENT_NAMES } from 'ozaria/site/components/teacher-dashboard/common/constants.js'

export default {
  namespaced: true,

  state: {
    teacherId: '',
    classroomId: '', // current classrom id for single class page and projects page
    selectedCourseIdForClassroom: {}, // selectedCourse for each classroomId across single class and student projects page
    loading: false,
    pageTitle: '',
    componentName: '',
    trackCategory: 'Teachers' // used for tracking events on pages shared between DSA and DT
  },

  mutations: {
    setTeacherId (state, teacherId) {
      if (state.teacherId !== teacherId) {
        state.teacherId = teacherId
      }
    },
    setClassroomId (state, classroomId) {
      if (state.classroomId !== classroomId) {
        state.classroomId = classroomId
      }
    },
    startLoading (state) {
      state.loading = true
    },
    stopLoading (state) {
      state.loading = false
    },
    resetLoadingState (state) {
      state.loading = false
    },
    setSelectedCourseIdCurrentClassroom (state, { courseId }) {
      if (state.classroomId) {
        Vue.set(state.selectedCourseIdForClassroom, state.classroomId, courseId)
      }
    },
    setPageTitle (state, title) {
      state.pageTitle = title
    },
    setComponentName (state, componentName) {
      state.componentName = componentName
    },
    setTrackCategory (state, trackCategory) {
      state.trackCategory = trackCategory
    }
  },

  getters: {
    teacherId (state) {
      return state.teacherId
    },
    classroomId (state) {
      return state.classroomId
    },
    classroom (state, getters) {
      return getters.getActiveClassrooms.find((c) => c._id === state.classroomId) || {}
    },
    getPageTitle (state) {
      return Vue.t(`teacher_dashboard.${state.pageTitle}`) || state.pageTitle
    },
    getComponentName (state) {
      return state.componentName
    },
    getLoadingState (state) {
      return state.loading
    },
    getTrackCategory (state) {
      return state.trackCategory
    },
    getActiveClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getActiveClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getArchivedClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getArchivedClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getSharedClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getSharedClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.teacherId && state.classroomId) {
        let classrooms = [
          ...(rootGetters['classrooms/getActiveClassroomsByTeacher'](state.teacherId) || []),
          ...(rootGetters['classrooms/getSharedClassroomsByTeacher'](state.teacherId) || [])
        ]
        if (!classrooms || classrooms.length === 0){
          return rootGetters['classrooms/getClassroomById'](state.classroomId) || {}
        }
        return classrooms.find((c) => c._id === state.classroomId) || {}
      } else {
        return {}
      }
    },
    getCoursesCurrentClassroom (state, getters, _rootState, rootGetters) {
      if (state.classroomId) {
        const classroom = getters['getCurrentClassroom']
        const classroomCourseIds = (classroom.courses || []).map((c) => c._id) || []
        const courses = rootGetters['courses/sorted'] || []
        return courses.filter((c) => classroomCourseIds.includes(c._id))
      }
      return []
    },
    getSelectedCourseIdCurrentClassroom (state, getters) {
      if (state.classroomId && state.selectedCourseIdForClassroom[state.classroomId]) {
        return state.selectedCourseIdForClassroom[state.classroomId]
      } else { // TODO default should be last assigned course
        const classroomCourses = getters['getCoursesCurrentClassroom'] || []
        if (classroomCourses.length > 0) {
          return (classroomCourses[0] || {})._id
        }
      }
    },
    getMembersCurrentClassroom (state, getters, _rootState, rootGetters) {
      if (state.classroomId) {
        const classroom = getters['getCurrentClassroom']
        return rootGetters['users/getClassroomMembers'](classroom) || []
      }
      return []
    },
    getLevelSessionsMapCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.classroomId) {
        return rootGetters['levelSessions/getSessionsMapForClassroom'](state.classroomId) || {}
      }
      return {}
    },
    getGameContentCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.classroomId) {
        return rootGetters['gameContent/getContentForClassroom'](state.classroomId) || {}
      }
      return {}
    },
    getActiveLicenses (state, _getters, _rootState, rootGetters) {
      if (state) {
        return rootGetters['prepaids/getActiveLicensesForTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getExpiredLicenses (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['prepaids/getExpiredLicensesForTeacher'](state.teacherId) || []
      } else {
        return []
      }
    }
  },

  actions: {
    // componentName = name of the vue component -> used to fetch relevant data for the respective page
    // options = { data: {} }
    // options.data = {} -> contains specific properties to fetch (or `project`) for an object as a string, eg: {users: 'firstName,lastName,email', levelSessions: 'state.complete,level,creator,changed'}
    async fetchData ({ state, dispatch, commit }, { componentName, options = {} }) {
      if (!state.teacherId) {
        console.error('Error in fetching data: teacherId is not set')
        noty({ text: 'Error in fetching data', type: 'error', layout: 'center', timeout: 2000 })
        return
      }

      commit('startLoading')
      commit('setComponentName', componentName)
      try {
        if (componentName === COMPONENT_NAMES.MY_CLASSES_ALL) {
          // My classes page
          await dispatch('fetchDataAllClasses', options)
          dispatch('fetchDataAllClassesAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.MY_CLASSES_SINGLE) {
          // Single class progress page
          await dispatch('fetchDataSingleClass', options)
          dispatch('fetchDataSingleClassAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.STUDENT_PROJECTS) {
          // Students progress page
          await dispatch('fetchDataStudentProjects', options)
          dispatch('fetchDataStudentProjectsAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.MY_LICENSES) {
          // Teacher licenses page
          await dispatch('fetchDataMyLicenses', options)
          dispatch('fetchDataMyLicensesAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.RESOURCE_HUB) {
          // Resource Hub page
          dispatch('fetchDataResourceHubAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.PD) {
          // PD page
          await dispatch('fetchDataPDAsync', options)
        }
      } catch (err) {
        console.error('Error in fetching data:', err)
        noty({ text: 'Error in fetching data', type: 'error', layout: 'topCenter', timeout: 2000 })
      } finally {
        commit('stopLoading')
        dispatch('classrooms/setMostRecentClassroomId', state.classroomId, { root: true })
        if (options.loadedEventName) { // should be set for tracking the loaded event for dashboard pages
          window.tracker?.trackEvent(options.loadedEventName, { category: state.trackCategory })
        }
      }
    },

    // My classes page
    // options.data = { levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataAllClasses ({ state, dispatch, rootGetters }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courseInstances/fetchCourseInstancesForTeacher', state.teacherId, { root: true }))
      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))

      await Promise.all(fetchPromises)
    },

    // My classes page - without blocking loading indicator
    async fetchDataAllClassesAsync ({ state, dispatch, rootGetters }, options = {}) {
      const fetchPromises = []

      const classrooms = rootGetters['classrooms/getClassroomsByTeacher'](state.teacherId)
      if (((classrooms || {}).active || []).length > 0) {
        classrooms.active.forEach((classroom) => {
          const levelSessionOptions = {
            project: (options.data || {}).levelSessions
          }
          fetchPromises.push(dispatch('levelSessions/fetchForClassroomMembers', { classroom, options: levelSessionOptions }, { root: true }))
        })
      }

      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Single class progress page
    // options.data = { users: '', levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataSingleClass ({ state, dispatch }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courseInstances/fetchCourseInstancesForClassroom', state.classroomId, { root: true }))
      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))

      options.fetchInteractiveSessions = true
      fetchPromises.push(dispatch('teacherDashboard/fetchClassroomData', options, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Single class progress page - without blocking loading indicator
    async fetchDataSingleClassAsync ({ state, dispatch, getters }, options = {}) {
      const fetchPromises = []

      let isSharedClass = false
      let teacherId = state.teacherId
      const classroom = getters['getCurrentClassroom']
      if (classroom) {
        isSharedClass = (classroom.permissions || []).find((p) => p.target === me.get('_id'))
        if (isSharedClass) {
          teacherId = classroom.ownerID
        }
      }
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: teacherId, sharedClassroomId: (isSharedClass ? state.classroomId : null) }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Students progress page
    // options.data = { users: '', levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataStudentProjects ({ state, dispatch }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchClassroomData', options, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Students progress page - without blocking loading indicator
    async fetchDataStudentProjectsAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      await Promise.all(fetchPromises)
    },

    // Teacher licenses page
    async fetchDataMyLicenses ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Teacher licenses page - without blocking loading indicator
    async fetchDataMyLicensesAsync ({ state, dispatch, getters }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))

      const licenses = getters['getActiveLicenses'].concat(getters['getExpiredLicenses'])
      const licenseIds = (licenses || []).map((l) => l._id)

      licenseIds.forEach((id) => {
        fetchPromises.push(dispatch('users/fetchCreatorOfPrepaid', id, { root: true }))
        fetchPromises.push(dispatch('prepaids/fetchJoinersForPrepaid', id, { root: true }))
      })
      await Promise.all(fetchPromises)
    },

    // Resource Hub Page
    async fetchDataResourceHubAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      // Note: Why do we need all the classes on the resource page?
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))
      await Promise.all(fetchPromises)
    },

    // PD Page
    async fetchDataPDAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      // TODO: do we need to fetch any data?
      await Promise.all(fetchPromises)
    },

    // Curriculum guides panel
    async fetchDataCurriculumGuide ({ dispatch, rootGetters }) {
      let sortedCourses = rootGetters['courses/sorted'] || []
      if (sortedCourses.length === 0) {
        await dispatch('courses/fetchReleased', undefined, { root: true })
      }
      sortedCourses = rootGetters['courses/sorted'] || []
      if (sortedCourses[0]) {
        // After loading ensure that the first course is automatically selected
        dispatch('baseCurriculumGuide/setSelectedCampaign', sortedCourses[0].campaignID, { root: true })
      }
      sortedCourses.forEach(({ campaignID }) => {
        dispatch('gameContent/fetchGameContentForCampaign', { campaignId: campaignID }, { root: true })
      })
    },

    // Fetches classroom data for current state.classroomId
    async fetchClassroomData ({ state, dispatch, rootGetters }, options = {}) {
      if (!state.classroomId) {
        console.error('Error in fetching data: classroomId is not set')
        noty({ text: 'Error in fetching data', type: 'error', layout: 'center', timeout: 2000 })
        return
      }

      const fetchPromises = []

      fetchPromises.push(dispatch('gameContent/fetchGameContentForClassroom', { classroomId: state.classroomId, options }, { root: true }))

      const classroom = rootGetters['classrooms/getClassroomById'](state.classroomId)
      if (classroom) {
        const userOptions = {
          project: (options.data || {}).users
        }
        fetchPromises.push(dispatch('users/fetchClassroomMembers', { classroom, options: userOptions }, { root: true }))
        const levelSessionOptions = {
          project: (options.data || {}).levelSessions
        }
        fetchPromises.push(dispatch('levelSessions/fetchForClassroomMembers', { classroom, options: levelSessionOptions }, { root: true }))
        if (options.fetchInteractiveSessions) {
          fetchPromises.push(dispatch('interactives/fetchSessionsForClassroomMembers', classroom, { root: true }))
        }
      }

      // TODO If classroom already loaded, load it asynchronously without blocking UI, i.e. without `await` to optimize performance
      await Promise.all(fetchPromises)
    }
  }
}
<commit_msg>DT: open to last-viewed chapter (#788)<commit_after>
import { COMPONENT_NAMES } from 'ozaria/site/components/teacher-dashboard/common/constants.js'

function getLastSelectedCourseKey (state) {
  return `courseId_${state.teacherId}_${state.classroomId}`
}

export default {
  namespaced: true,

  state: {
    teacherId: '',
    classroomId: '', // current classrom id for single class page and projects page
    selectedCourseIdForClassroom: {}, // selectedCourse for each classroomId across single class and student projects page
    loading: false,
    pageTitle: '',
    componentName: '',
    trackCategory: 'Teachers' // used for tracking events on pages shared between DSA and DT
  },

  mutations: {
    setTeacherId (state, teacherId) {
      if (state.teacherId !== teacherId) {
        state.teacherId = teacherId
      }
    },
    setClassroomId (state, classroomId) {
      if (state.classroomId !== classroomId) {
        state.classroomId = classroomId
      }
    },
    startLoading (state) {
      state.loading = true
    },
    stopLoading (state) {
      state.loading = false
    },
    resetLoadingState (state) {
      state.loading = false
    },
    setSelectedCourseIdCurrentClassroom (state, { courseId }) {
      if (state.classroomId) {
        localStorage.setItem(getLastSelectedCourseKey(state), courseId);
        Vue.set(state.selectedCourseIdForClassroom, state.classroomId, courseId)
      }
    },
    setPageTitle (state, title) {
      state.pageTitle = title
    },
    setComponentName (state, componentName) {
      state.componentName = componentName
    },
    setTrackCategory (state, trackCategory) {
      state.trackCategory = trackCategory
    }
  },

  getters: {
    teacherId (state) {
      return state.teacherId
    },
    classroomId (state) {
      return state.classroomId
    },
    classroom (state, getters) {
      return getters.getActiveClassrooms.find((c) => c._id === state.classroomId) || {}
    },
    getPageTitle (state) {
      return Vue.t(`teacher_dashboard.${state.pageTitle}`) || state.pageTitle
    },
    getComponentName (state) {
      return state.componentName
    },
    getLoadingState (state) {
      return state.loading
    },
    getTrackCategory (state) {
      return state.trackCategory
    },
    getActiveClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getActiveClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getArchivedClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getArchivedClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getSharedClassrooms (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['classrooms/getSharedClassroomsByTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.teacherId && state.classroomId) {
        let classrooms = [
          ...(rootGetters['classrooms/getActiveClassroomsByTeacher'](state.teacherId) || []),
          ...(rootGetters['classrooms/getSharedClassroomsByTeacher'](state.teacherId) || [])
        ]
        if (!classrooms || classrooms.length === 0){
          return rootGetters['classrooms/getClassroomById'](state.classroomId) || {}
        }
        return classrooms.find((c) => c._id === state.classroomId) || {}
      } else {
        return {}
      }
    },
    getCoursesCurrentClassroom (state, getters, _rootState, rootGetters) {
      if (state.classroomId) {
        const classroom = getters['getCurrentClassroom']
        const classroomCourseIds = (classroom.courses || []).map((c) => c._id) || []
        const courses = rootGetters['courses/sorted'] || []
        return courses.filter((c) => classroomCourseIds.includes(c._id))
      }
      return []
    },
    getSelectedCourseIdCurrentClassroom (state, getters) {
      if (state.classroomId && state.selectedCourseIdForClassroom[state.classroomId]) {
        return state.selectedCourseIdForClassroom[state.classroomId]
      } else {

        const savedCourseId = localStorage.getItem(getLastSelectedCourseKey(state));
        if(savedCourseId){
          return savedCourseId;
        }

        const classroomCourses = getters['getCoursesCurrentClassroom'] || []
        if (classroomCourses.length > 0) {
          return (classroomCourses[0] || {})._id
        }
      }
    },
    getMembersCurrentClassroom (state, getters, _rootState, rootGetters) {
      if (state.classroomId) {
        const classroom = getters['getCurrentClassroom']
        return rootGetters['users/getClassroomMembers'](classroom) || []
      }
      return []
    },
    getLevelSessionsMapCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.classroomId) {
        return rootGetters['levelSessions/getSessionsMapForClassroom'](state.classroomId) || {}
      }
      return {}
    },
    getGameContentCurrentClassroom (state, _getters, _rootState, rootGetters) {
      if (state.classroomId) {
        return rootGetters['gameContent/getContentForClassroom'](state.classroomId) || {}
      }
      return {}
    },
    getActiveLicenses (state, _getters, _rootState, rootGetters) {
      if (state) {
        return rootGetters['prepaids/getActiveLicensesForTeacher'](state.teacherId) || []
      } else {
        return []
      }
    },
    getExpiredLicenses (state, _getters, _rootState, rootGetters) {
      if (state.teacherId) {
        return rootGetters['prepaids/getExpiredLicensesForTeacher'](state.teacherId) || []
      } else {
        return []
      }
    }
  },

  actions: {
    // componentName = name of the vue component -> used to fetch relevant data for the respective page
    // options = { data: {} }
    // options.data = {} -> contains specific properties to fetch (or `project`) for an object as a string, eg: {users: 'firstName,lastName,email', levelSessions: 'state.complete,level,creator,changed'}
    async fetchData ({ state, dispatch, commit }, { componentName, options = {} }) {
      if (!state.teacherId) {
        console.error('Error in fetching data: teacherId is not set')
        noty({ text: 'Error in fetching data', type: 'error', layout: 'center', timeout: 2000 })
        return
      }

      commit('startLoading')
      commit('setComponentName', componentName)
      try {
        if (componentName === COMPONENT_NAMES.MY_CLASSES_ALL) {
          // My classes page
          await dispatch('fetchDataAllClasses', options)
          dispatch('fetchDataAllClassesAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.MY_CLASSES_SINGLE) {
          // Single class progress page
          await dispatch('fetchDataSingleClass', options)
          dispatch('fetchDataSingleClassAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.STUDENT_PROJECTS) {
          // Students progress page
          await dispatch('fetchDataStudentProjects', options)
          dispatch('fetchDataStudentProjectsAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.MY_LICENSES) {
          // Teacher licenses page
          await dispatch('fetchDataMyLicenses', options)
          dispatch('fetchDataMyLicensesAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.RESOURCE_HUB) {
          // Resource Hub page
          dispatch('fetchDataResourceHubAsync', options) // does not block loading indicator
        } else if (componentName === COMPONENT_NAMES.PD) {
          // PD page
          await dispatch('fetchDataPDAsync', options)
        }
      } catch (err) {
        console.error('Error in fetching data:', err)
        noty({ text: 'Error in fetching data', type: 'error', layout: 'topCenter', timeout: 2000 })
      } finally {
        commit('stopLoading')
        dispatch('classrooms/setMostRecentClassroomId', state.classroomId, { root: true })
        if (options.loadedEventName) { // should be set for tracking the loaded event for dashboard pages
          window.tracker?.trackEvent(options.loadedEventName, { category: state.trackCategory })
        }
      }
    },

    // My classes page
    // options.data = { levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataAllClasses ({ state, dispatch, rootGetters }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courseInstances/fetchCourseInstancesForTeacher', state.teacherId, { root: true }))
      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))

      await Promise.all(fetchPromises)
    },

    // My classes page - without blocking loading indicator
    async fetchDataAllClassesAsync ({ state, dispatch, rootGetters }, options = {}) {
      const fetchPromises = []

      const classrooms = rootGetters['classrooms/getClassroomsByTeacher'](state.teacherId)
      if (((classrooms || {}).active || []).length > 0) {
        classrooms.active.forEach((classroom) => {
          const levelSessionOptions = {
            project: (options.data || {}).levelSessions
          }
          fetchPromises.push(dispatch('levelSessions/fetchForClassroomMembers', { classroom, options: levelSessionOptions }, { root: true }))
        })
      }

      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Single class progress page
    // options.data = { users: '', levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataSingleClass ({ state, dispatch }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courseInstances/fetchCourseInstancesForClassroom', state.classroomId, { root: true }))
      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))

      options.fetchInteractiveSessions = true
      fetchPromises.push(dispatch('teacherDashboard/fetchClassroomData', options, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Single class progress page - without blocking loading indicator
    async fetchDataSingleClassAsync ({ state, dispatch, getters }, options = {}) {
      const fetchPromises = []

      let isSharedClass = false
      let teacherId = state.teacherId
      const classroom = getters['getCurrentClassroom']
      if (classroom) {
        isSharedClass = (classroom.permissions || []).find((p) => p.target === me.get('_id'))
        if (isSharedClass) {
          teacherId = classroom.ownerID
        }
      }
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: teacherId, sharedClassroomId: (isSharedClass ? state.classroomId : null) }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Students progress page
    // options.data = { users: '', levelSessions: '' } -> properties needed for these objects, i.e. will be used as `project` in db queries
    async fetchDataStudentProjects ({ state, dispatch }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('courses/fetchReleased', undefined, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchClassroomData', options, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Students progress page - without blocking loading indicator
    async fetchDataStudentProjectsAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      await Promise.all(fetchPromises)
    },

    // Teacher licenses page
    async fetchDataMyLicenses ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))

      await Promise.all(fetchPromises)
    },

    // Teacher licenses page - without blocking loading indicator
    async fetchDataMyLicensesAsync ({ state, dispatch, getters }, options = {}) {
      const fetchPromises = []

      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))

      const licenses = getters['getActiveLicenses'].concat(getters['getExpiredLicenses'])
      const licenseIds = (licenses || []).map((l) => l._id)

      licenseIds.forEach((id) => {
        fetchPromises.push(dispatch('users/fetchCreatorOfPrepaid', id, { root: true }))
        fetchPromises.push(dispatch('prepaids/fetchJoinersForPrepaid', id, { root: true }))
      })
      await Promise.all(fetchPromises)
    },

    // Resource Hub Page
    async fetchDataResourceHubAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      fetchPromises.push(dispatch('prepaids/fetchPrepaidsForTeacher', { teacherId: state.teacherId }, { root: true }))
      fetchPromises.push(dispatch('teacherDashboard/fetchDataCurriculumGuide', undefined, { root: true }))
      // Note: Why do we need all the classes on the resource page?
      fetchPromises.push(dispatch('classrooms/fetchClassroomsForTeacher', { teacherId: state.teacherId }, { root: true }))
      await Promise.all(fetchPromises)
    },

    // PD Page
    async fetchDataPDAsync ({ state, dispatch }, options = {}) {
      const fetchPromises = []
      // TODO: do we need to fetch any data?
      await Promise.all(fetchPromises)
    },

    // Curriculum guides panel
    async fetchDataCurriculumGuide ({ dispatch, rootGetters }) {
      let sortedCourses = rootGetters['courses/sorted'] || []
      if (sortedCourses.length === 0) {
        await dispatch('courses/fetchReleased', undefined, { root: true })
      }
      sortedCourses = rootGetters['courses/sorted'] || []
      if (sortedCourses[0]) {
        // After loading ensure that the first course is automatically selected
        dispatch('baseCurriculumGuide/setSelectedCampaign', sortedCourses[0].campaignID, { root: true })
      }
      sortedCourses.forEach(({ campaignID }) => {
        dispatch('gameContent/fetchGameContentForCampaign', { campaignId: campaignID }, { root: true })
      })
    },

    // Fetches classroom data for current state.classroomId
    async fetchClassroomData ({ state, dispatch, rootGetters }, options = {}) {
      if (!state.classroomId) {
        console.error('Error in fetching data: classroomId is not set')
        noty({ text: 'Error in fetching data', type: 'error', layout: 'center', timeout: 2000 })
        return
      }

      const fetchPromises = []

      fetchPromises.push(dispatch('gameContent/fetchGameContentForClassroom', { classroomId: state.classroomId, options }, { root: true }))

      const classroom = rootGetters['classrooms/getClassroomById'](state.classroomId)
      if (classroom) {
        const userOptions = {
          project: (options.data || {}).users
        }
        fetchPromises.push(dispatch('users/fetchClassroomMembers', { classroom, options: userOptions }, { root: true }))
        const levelSessionOptions = {
          project: (options.data || {}).levelSessions
        }
        fetchPromises.push(dispatch('levelSessions/fetchForClassroomMembers', { classroom, options: levelSessionOptions }, { root: true }))
        if (options.fetchInteractiveSessions) {
          fetchPromises.push(dispatch('interactives/fetchSessionsForClassroomMembers', classroom, { root: true }))
        }
      }

      // TODO If classroom already loaded, load it asynchronously without blocking UI, i.e. without `await` to optimize performance
      await Promise.all(fetchPromises)
    }
  }
}
<|endoftext|>


<commit_before>/**
 * Dashboards' model
 */

angular.module(PKG.name+'.feature.dashboard').factory('myDashboardsModel',
function (Widget, MyDataSource, $timeout) {

  var dSrc = new MyDataSource(),
      API_PATH = '/configuration/dashboards';

  function Dashboard (p) {
    angular.extend(
      this,
      {
        id: p.id,
        title: p.title || 'Dashboard',
        columns: []
      }
    );

    if(angular.isArray(p.columns)) {
      angular.forEach(p.columns, function (c) {
        this.push(c.map(function (o) {
          return new Widget(o);
        }));
      }, this.columns);
    }
    else {
      // default is a single empty column
      this.columns.push([]);
    }
  }

  /**
   * rename a widget
   */
  Dashboard.prototype.renameWidget = function (widget, newName) {
    if(newName) {
      widget.title = newName;
      this.persist();
    }
  };

  /**
   * remove a widget from the active dashboard tab
   * @param  {object} w the widget object
   */
  Dashboard.prototype.removeWidget = function (widget) {
    var that = this;
    angular.forEach(that.columns, function (c, i) {
      that.columns[i] = c.filter(function (p) {
        return widget !== p;
      });
    });
    this.persist();
  };

  /**
   * add a widget to the active dashboard tab
   */
  Dashboard.prototype.addWidget = function (w) {
    var c = this.columns,
        index = 0,
        smallest;

    // find the column with the least widgets
    for (var i = 0; i < c.length; i++) {
      var len = c[i].length;
      if(smallest===undefined || (len < smallest)) {
        smallest = len;
        index = i;
      }
    }

    w = w || new Widget({
      title: 'just added'
    });

    this.columns[index].unshift(w);
    this.persist();
  };

  /**
   * rename dashboard tab
   */
  Dashboard.prototype.rename = function (newName) {
    if(newName) {
      this.title = newName;
      this.persist();
    }
  };

  /**
   * benefit from angular's toJsonReplacer
   */
  Dashboard.prototype.properties = function () {
    return angular.fromJson(angular.toJson(this));
  };

  /**
   * save to backend
   */
  Dashboard.prototype.persist = function () {
    var body = this.properties();

    if(this.id) { // updating
      dSrc.request(
        {
          method: 'PUT',
          _cdapNsPath: API_PATH + '/' + this.id,
          body: body
        }
      );
    }
    else { // saving
      dSrc.request(
        {
          method: 'POST',
          _cdapNsPath: API_PATH,
          body: body
        },
        (function (result) {
          this.id = result.id;
        }).bind(this)
      );
    }
  };

/* ------------------------------------------------------------------------- */

  function Model () {
    var data = [];
    data.activeIndex = 0;

    this.data = data;

    dSrc.request(
      {
        method: 'GET',
        _cdapNsPath: API_PATH
      },
      function (result) {

        angular.forEach(result, function (v) {
          data.push(new Dashboard(angular.extend(
            {
              id: v.id
            },
            v.config
          )));

        });
      }
    );

  }

  /**
   * direct access to the current tab's Dashboard instance
   */
  Model.prototype.current = function () {
    return this.data[this.data.activeIndex || 0];
  };

  /**
   * remove a dashboard tab
   */
  Model.prototype.remove = function (index) {
    var removed = this.data.splice(index, 1)[0];

    dSrc.request(
      {
        method: 'DELETE',
        _cdapNsPath: API_PATH + '/' + removed.id
      }
    );
  };

  /**
   * add a new dashboard tab
   */
  Model.prototype.add = function (properties, colCount) {
    var d = new Dashboard(properties);

    // add columns as needed
    for (var i = 1; i < (colCount||3); i++) {
      d.columns.push([]);
    }

    // default widget in first column
    // FIXME: figure out why asSortable throws an error without $timeout
    $timeout(function () {
      d.columns[0].push(new Widget());

      // save to backend
      d.persist();
    });

    // newly created tab becomes active
    var n = this.data.push(d);
    this.data.activeIndex = n-1;
  };

  return new Model();
});

<commit_msg>dont use extend inline<commit_after>/**
 * Dashboards' model
 */

angular.module(PKG.name+'.feature.dashboard').factory('myDashboardsModel',
function (Widget, MyDataSource, $timeout) {

  var dSrc = new MyDataSource(),
      API_PATH = '/configuration/dashboards';

  function Dashboard (p) {
    angular.extend(
      this,
      {
        id: p.id,
        title: p.title || 'Dashboard',
        columns: []
      }
    );

    if(angular.isArray(p.columns)) {
      angular.forEach(p.columns, function (c) {
        this.push(c.map(function (o) {
          return new Widget(o);
        }));
      }, this.columns);
    }
    else {
      // default is a single empty column
      this.columns.push([]);
    }
  }

  /**
   * rename a widget
   */
  Dashboard.prototype.renameWidget = function (widget, newName) {
    if(newName) {
      widget.title = newName;
      this.persist();
    }
  };

  /**
   * remove a widget from the active dashboard tab
   * @param  {object} w the widget object
   */
  Dashboard.prototype.removeWidget = function (widget) {
    var that = this;
    angular.forEach(that.columns, function (c, i) {
      that.columns[i] = c.filter(function (p) {
        return widget !== p;
      });
    });
    this.persist();
  };

  /**
   * add a widget to the active dashboard tab
   */
  Dashboard.prototype.addWidget = function (w) {
    var c = this.columns,
        index = 0,
        smallest;

    // find the column with the least widgets
    for (var i = 0; i < c.length; i++) {
      var len = c[i].length;
      if(smallest===undefined || (len < smallest)) {
        smallest = len;
        index = i;
      }
    }

    w = w || new Widget({
      title: 'just added'
    });

    this.columns[index].unshift(w);
    this.persist();
  };

  /**
   * rename dashboard tab
   */
  Dashboard.prototype.rename = function (newName) {
    if(newName) {
      this.title = newName;
      this.persist();
    }
  };

  /**
   * benefit from angular's toJsonReplacer
   */
  Dashboard.prototype.properties = function () {
    return angular.fromJson(angular.toJson(this));
  };

  /**
   * save to backend
   */
  Dashboard.prototype.persist = function () {
    var body = this.properties();

    if(this.id) { // updating
      dSrc.request(
        {
          method: 'PUT',
          _cdapNsPath: API_PATH + '/' + this.id,
          body: body
        }
      );
    }
    else { // saving
      dSrc.request(
        {
          method: 'POST',
          _cdapNsPath: API_PATH,
          body: body
        },
        (function (result) {
          this.id = result.id;
        }).bind(this)
      );
    }
  };

/* ------------------------------------------------------------------------- */

  function Model () {
    var data = [];
    data.activeIndex = 0;

    this.data = data;

    dSrc.request(
      {
        method: 'GET',
        _cdapNsPath: API_PATH
      },
      function (result) {

        angular.forEach(result, function (v) {
          var p = v.config;
          p.id = v.id;
          data.push(new Dashboard(p));
        });
      }
    );

  }

  /**
   * direct access to the current tab's Dashboard instance
   */
  Model.prototype.current = function () {
    return this.data[this.data.activeIndex || 0];
  };

  /**
   * remove a dashboard tab
   */
  Model.prototype.remove = function (index) {
    var removed = this.data.splice(index, 1)[0];

    dSrc.request(
      {
        method: 'DELETE',
        _cdapNsPath: API_PATH + '/' + removed.id
      }
    );
  };

  /**
   * add a new dashboard tab
   */
  Model.prototype.add = function (properties, colCount) {
    var d = new Dashboard(properties);

    // add columns as needed
    for (var i = 1; i < (colCount||3); i++) {
      d.columns.push([]);
    }

    // default widget in first column
    // FIXME: figure out why asSortable throws an error without $timeout
    $timeout(function () {
      d.columns[0].push(new Widget());

      // save to backend
      d.persist();
    });

    // newly created tab becomes active
    var n = this.data.push(d);
    this.data.activeIndex = n-1;
  };

  return new Model();
});

<|endoftext|>