diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index f43d00558e..7f0e270cdf 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -43,7 +43,9 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng $scope.discard(); Challenges.Challenge.query(); } else { - challenge._editing = false; + // TODO figure out a more elegant way about this + //challenge._editing = false; + $scope.locked = true; } }); }; @@ -61,7 +63,7 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng */ $scope["delete"] = function(challenge) { if (confirm("Delete challenge, are you sure?") !== true) return; - challenge.delete(); + challenge.$delete(); }; //------------------------------------------------------------ diff --git a/public/js/directives/directives.js b/public/js/directives/directives.js index 94ebc33417..b8618c7545 100644 --- a/public/js/directives/directives.js +++ b/public/js/directives/directives.js @@ -130,7 +130,6 @@ habitrpg scope.obj = scope[attrs.obj]; scope.main = attrs.main; - scope.lists = [ { header: 'Habits', @@ -154,7 +153,6 @@ habitrpg tasks: scope.obj.rewards } ]; - scope.editable = true; } } }]); diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 7b4467041e..903f68f5b8 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -39,19 +39,74 @@ api.create = function(req, res){ }); } +function keepAttrs(task) { + // only sync/compare important attrs + var keepAttrs = 'text notes up down priority repeat'.split(' '); + if (task.type=='reward') keepAttrs.push('value'); + return _.pick(task, keepAttrs); +} + // UPDATE api.update = function(req, res){ //FIXME sanitize - Challenge.findByIdAndUpdate(req.params.cid, {$set:req.body}, function(err, saved){ + var cid = req.params.cid; + async.waterfall([ + function(cb){ + // We first need the original challenge data, since we're going to compare against new & decide to sync users + Challenge.findById(cid, cb); + }, + function(chal, cb) { + + // Update the challenge, and then just res.json success (note we're passing `cb` here). + // The syncing stuff is really heavy, and the client doesn't care - so we kick it off in the background + delete req.body._id; + Challenge.findByIdAndUpdate(cid, {$set:req.body}, cb); + + // Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers + function comparableData(obj) { + return ( + _.chain(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards)) + .sortBy('id') // we don't want to update if they're sort-order is different + .transform(function(result, task){ + result.push(keepAttrs(task)); + })) + .toString(); // for comparing arrays easily + } + if (comparableData(chal) !== comparableData(req.body)) { + User.find({_id: {$in: chal.members}}, function(err, users){ + console.log('Challenge updated, sync to subscribers'); + if (err) throw err; + _.each(users, function(user){ + syncChalToUser(chal, user); + user.save(); + }) + }) + } + + } + ], function(err, saved){ if(err) res.json(500, {err:err}); res.json(saved); - // TODO update subscribed users' tasks, each user.__v++ }) } // DELETE -// 1. update challenge -// 2. update sub'd users' tasks +api['delete'] = function(req, res){ + Challenge.findOneAndRemove({_id:req.params.cid}, function(err, removed){ + if (err) return res.json(500, {err: err}); + User.find({_id:{$in: removed.members}}, function(err, users){ + if (err) throw err; + _.each(users, function(user){ + _.each(user.tasks, function(task){ + if (task.challenge && task.challenge.id == removed._id) { + task.challenge.broken = 'CHALLENGE_DELETED'; + } + }) + user.save(); + }) + }) + }) +} /** * Syncs all new tasks, deleted tasks, etc to the user object @@ -79,18 +134,23 @@ var syncChalToUser = function(chal, user) { } tags = {}; tags[chal._id] = true; - _.each(['habits','dailys','todos','rewards'], function(type){ - _.each(chal[type], function(task){ - _.defaults(task, {tags: tags, challenge:{}}); - _.defaults(task.challenge, {id:chal._id, broken:false}); - if (~(i = _.findIndex(user[type], {id:task.id}))) { - _.defaults(user[type][i], task); - } else { - user[type].push(task); - } - }) + + // Sync new tasks and updated tasks + _.each(chal.tasks, function(task){ + var type = task.type; + _.defaults(task, {tags: tags, challenge:{}}); + _.defaults(task.challenge, {id:chal._id, broken:false}); + if (user.tasks[task.id]) { + _.merge(user.tasks[task.id], keepAttrs(task)); + } else { + user[type+'s'].push(task); + } + }) + + // Flag deleted tasks as "broken" + _.each(user.tasks, function(task){ + if (!chal.tasks[task.id]) task.challenge.broken = 'TASK_DELETED'; }) - //FIXME account for deleted tasks (each users.tasks.broken = true) }; api.join = function(req, res){ diff --git a/src/models/challenge.js b/src/models/challenge.js index a74c25f39d..0631e648f4 100644 --- a/src/models/challenge.js +++ b/src/models/challenge.js @@ -28,6 +28,5 @@ ChallengeSchema.virtual('tasks').get(function () { return tasks; }); - module.exports.schema = ChallengeSchema; module.exports.model = mongoose.model("Challenge", ChallengeSchema); \ No newline at end of file diff --git a/src/routes/api.js b/src/routes/api.js index 16899b6b8a..0018f9d8cb 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -86,6 +86,8 @@ router.post('/market/buy', auth.auth, user.marketBuy); // without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource router.get('/challenges', auth.auth, challenges.get) router.post('/challenges', auth.auth, challenges.create) +router.post('/challenges/:cid', auth.auth, challenges.update) +router['delete']('/challenges/:cid', auth.auth, challenges['delete']) router.post('/challenges/:cid/join', auth.auth, challenges.join) router.post('/challenges/:cid/leave', auth.auth, challenges.leave) diff --git a/views/options/challenges.jade b/views/options/challenges.jade index 5e63f02efd..823978c81a 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -33,7 +33,7 @@ habitrpg-tasks(main=false, obj='newChallenge') // Challenges list - .accordion-group(ng-repeat='challenge in challenges') + .accordion-group(ng-repeat='challenge in challenges', ng-init='locked=true') .accordion-heading ul.pull-right.challenge-accordion-header-specs li {{challenge.members.length}} Subscribers @@ -57,21 +57,21 @@ .accordion-body.collapse(id='accordion-challenge-{{challenge._id}}') .accordion-inner // Edit button - span(style='position: absolute; right: 0;') - ul.nav.nav-pills(ng-show='challenge.leader==user._id && !challenge._editing') - li - a(ng-click='challenge._editing = true') - i.icon-pencil - ul.nav.nav-pills(ng-show='challenge._editing') - li - a(ng-click='save(challenge)') - i.icon-ok - div(ng-show='challenge._editing') + ul.unstyled() + li(ng-show='challenge.leader==user._id && locked') + button.btn.btn-default(ng-click='locked = false') Edit + li(ng-hide='locked') + button.btn.btn-primary(ng-click='save(challenge)') Save + button.btn.btn-danger(ng-click='delete(challenge)') Delete + button.btn.btn-default(ng-click='locked=true') Cancel + + div(ng-hide='locked') .-options input.option-content(type='text', ng-model='challenge.name') textarea.option-content(cols='3', placeholder='Description', ng-model='challenge.description') // - a.btn.btn-small.btn-danger(ng-click='delete(challenge)') Delete + hr + div(ng-if='challenge.description') {{challenge.description}} habitrpg-tasks(obj='challenge', main=false) h3 Statistics diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade index b43bd2df89..4d38ddccfc 100644 --- a/views/shared/tasks/lists.jade +++ b/views/shared/tasks/lists.jade @@ -31,7 +31,7 @@ script(id='templates/habitrpg-tasks.html', type="text/ng-template") .todos-chart(ng-if='list.type == "todo"', ng-show='charts.todos') // Add New - form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-show='editable', ng-hide='list.showCompleted && list.type=="todo"', ng-submit='addTask(list)') + form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-hide='locked || (list.showCompleted && list.type=="todo")', ng-submit='addTask(list)') span.addtask-field input(type='text', ng-model='list.newTask', placeholder='{{list.placeHolder}}', required) input.addtask-btn(type='submit', value='+', ng-disabled='new{{list.type}}form.$invalid') diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index d048e6a1c3..3e18c28a5a 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -61,19 +61,19 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, // Broken Challenge .well(ng-if='task.challenge.broken') - div(ng-if='task.challenge.broken==1') + div(ng-if='task.challenge.broken=="TASK_DELETED"') p Broken Challenge Link: this task was part of a challenge, but has been removed from it. What would you like to do? p a(ng-click="unlink(task, 'keep')") Keep It |  |  a(ng-click="remove(list, $index)") Remove It - div(ng-if='task.challenge.broken==2') + div(ng-if='task.challenge.broken=="CHALLENGE_DELETED"') p Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What to do with the orphan tasks? p a(ng-click="unlink(task 'keep-all')") Keep Them |  |  a(ng-click="unlink(task, 'remove-all')") Remove Them - div(ng-if='task.challenge.broken==3') + //-div(ng-if='task.challenge.broken=="UNSUBSCRIBED"') p Broken Challenge Link: this task was part of a challenge, but you have unsubscribed from the challenge. What to do with the orphan tasks? p a(ng-click="unlink(task, 'keep-all')") Keep Them