diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index c8989b1c35..9655cd0b6b 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notification', '$compile', 'Groups', - function($scope, User, Challenges, Notification, $compile, Groups) { +habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notification', '$compile', 'Groups', '$state', + function($scope, User, Challenges, Notification, $compile, Groups, $state) { // FIXME get this from cache Groups.Group.query(function(groups){ @@ -20,7 +20,7 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica * Create */ $scope.create = function() { - $scope.newChallenge = new Challenges.Challenge({ + $scope.obj = $scope.newChallenge = new Challenges.Challenge({ name: '', description: '', habits: [], @@ -40,11 +40,11 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica $scope.save = function(challenge) { if (!challenge.group) return alert('Please select group'); var isNew = !challenge._id; - challenge.$save(function(){ + challenge.$save(function(_challenge){ if (isNew) { Notification.text('Challenge Created'); $scope.discard(); - Challenges.Challenge.query(); + $scope.challenges.unshift(_challenge); } else { // TODO figure out a more elegant way about this //challenge._editing = false; @@ -64,9 +64,13 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica /** * Delete */ - $scope["delete"] = function(challenge) { + $scope["delete"] = function(challenge, $index) { if (confirm("Delete challenge, are you sure?") !== true) return; - challenge.$delete(); + challenge.$delete(function(){ + $state.go('options.challenges'); + $scope.challenges = Challenges.Challenge.query(); + User.log({}); + }); }; //------------------------------------------------------------ @@ -97,29 +101,43 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica /* -------------------------- - Unsubscribe functions + Subscription -------------------------- */ - $scope.unsubscribe = function(keep) { + $scope.join = function(challenge){ + challenge.$join(function(){ + User.log({}); + }); + + } + + $scope.leave = function(keep) { if (keep == 'cancel') { $scope.selectedChal = undefined; } else { - $scope.selectedChal.$leave({keep:keep}); + $scope.selectedChal.$leave({keep:keep}, function(){ + User.log({}); + }); } $scope.popoverEl.popover('destroy'); } - $scope.clickUnsubscribe = function(chal, $event) { + + /** + * Named "clickLeave" to distinguish between "actual" leave above, since this triggers the + * "are you sure?" dialog. + */ + $scope.clickLeave = function(chal, $event) { $scope.selectedChal = chal; $scope.popoverEl = $($event.target); var html = $compile( - 'Remove Tasks
\nKeep Tasks
\nCancel
' + 'Remove Tasks
\nKeep Tasks
\nCancel
' )($scope); $scope.popoverEl.popover('destroy').popover({ html: true, placement: 'top', trigger: 'manual', - title: 'Unsubscribe From Challenge And:', + title: 'Leave challenge and...', content: html }).popover('show'); } diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 6aed897abb..0e703b1f43 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -26,10 +26,15 @@ api.list = function(req, res) { {group: 'habitrpg'} ] }) - .select('name description memberCount groups') + .select('name description memberCount groups members') .populate('groups', '_id name') .exec(function(err, challenges){ if (err) return res.json(500,{err:err}); + _.each(challenges, function(c){ + if (~c.members.indexOf(user._id)) + c._isMember = true; + c.members = []; + }) res.json(challenges); }); } @@ -43,6 +48,8 @@ api.get = function(req, res) { if(err) return res.json(500, {err:err}); // slim down the return members' tasks to only the ones in the challenge _.each(challenge.members, function(member){ + if (member._id == user._id) + challenge._isMember = true; _.each(['habits', 'dailys', 'todos', 'rewards'], function(type){ member[type] = _.where(member[type], function(task){ return task.challenge && task.challenge.id == challenge._id; @@ -58,9 +65,8 @@ api.create = function(req, res){ // FIXME sanitize var challenge = new Challenge(req.body); challenge.save(function(err, saved){ - // Need to create challenge with refs (group, leader)? Or is this taken care of automatically? - // @see http://mongoosejs.com/docs/populate.html if (err) return res.json(500, {err:err}); + Group.findByIdAndUpdate(saved.group, {$addToSet:{challenges:saved._id}}) // fixme error-check res.json(saved); }); } @@ -118,19 +124,32 @@ api.update = function(req, res){ // DELETE 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; + var removed; + async.waterfall([ + function(cb){ + Challenge.findOneAndRemove({_id:req.params.cid}, cb) + }, + function(_removed, cb) { + removed = _removed; + User.find({_id:{$in: removed.members}}, cb); + }, + function(users, cb) { + var parallel = []; _.each(users, function(user){ _.each(user.tasks, function(task){ if (task.challenge && task.challenge.id == removed._id) { task.challenge.broken = 'CHALLENGE_DELETED'; } }) - user.save(); + parallel.push(function(cb2){ + user.save(cb2); + }) }) - }) + async.parallel(parallel, cb); + } + ], function(err){ + if (err) return res.json(500, {err: err}); + res.send(200); }) } @@ -199,6 +218,7 @@ api.join = function(req, res){ } ], function(err, result){ if(err) return res.json(500,{err:err}); + result._isMember = true; res.json(result); }); } @@ -206,7 +226,7 @@ api.join = function(req, res){ function unlink(user, cid, keep, tid) { switch (keep) { case 'keep': - delete user.tasks[tid].challenge; + user.tasks[tid].challenge = {}; break; case 'remove': user[user.tasks[tid].type+'s'].id(tid).remove(); @@ -214,7 +234,7 @@ function unlink(user, cid, keep, tid) { case 'keep-all': _.each(user.tasks, function(t){ if (t.challenge && t.challenge.id == cid) { - delete t.challenge; + t.challenge = {}; } }); break; @@ -249,6 +269,7 @@ api.leave = function(req, res){ } ], function(err, result){ if(err) return res.json(500,{err:err}); + result._isMember = false; res.json(result); }); } @@ -264,6 +285,10 @@ api.unlink = function(req, res, next) { if (!req.query.keep) return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'}); unlink(user, cid, req.query.keep, tid); + user.markModified('habits'); + user.markModified('dailys'); + user.markModified('todos'); + user.markModified('rewards'); user.save(function(err, saved){ if (err) return res.json(500,{err:err}); res.send(200); diff --git a/src/models/challenge.js b/src/models/challenge.js index fb77bd1bba..a3c60edc9c 100644 --- a/src/models/challenge.js +++ b/src/models/challenge.js @@ -3,6 +3,7 @@ var Schema = mongoose.Schema; var helpers = require('habitrpg-shared/script/helpers'); var _ = require('lodash'); var TaskSchema = require('./task').schema; +var Group = require('./group').model; var ChallengeSchema = new Schema({ _id: {type: String, 'default': helpers.uuid}, @@ -20,7 +21,7 @@ var ChallengeSchema = new Schema({ //}, timestamp: {type: Date, 'default': Date.now}, members: [{type: String, ref: 'User'}], - memberCount: [{type: Number, 'default': 0}] + memberCount: {type: Number, 'default': 0} }); ChallengeSchema.virtual('tasks').get(function () { @@ -29,10 +30,19 @@ ChallengeSchema.virtual('tasks').get(function () { return tasks; }); +// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate() +// @see https://github.com/LearnBoost/mongoose/issues/964 ChallengeSchema.pre('save', function(next){ this.memberCount = _.size(this.members); - next(); + next() }) +ChallengeSchema.methods.toJSON = function(){ + var doc = this.toObject(); + doc.memberCount = _.size(doc.members); // @see pre('save') comment above + doc._isMember = this._isMember; + return doc; +} + module.exports.schema = ChallengeSchema; module.exports.model = mongoose.model("Challenge", ChallengeSchema); \ No newline at end of file diff --git a/src/models/group.js b/src/models/group.js index 9393e94c9b..bf2227ce0d 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -58,6 +58,8 @@ function removeDuplicates(doc){ } } +// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate() +// @see https://github.com/LearnBoost/mongoose/issues/964 GroupSchema.pre('save', function(next){ removeDuplicates(this); this.memberCount = _.size(this.members); @@ -69,6 +71,11 @@ GroupSchema.methods.toJSON = function(){ var doc = this.toObject(); removeDuplicates(doc); doc._isMember = this._isMember; + + // @see pre('save') comment above + this.memberCount = _.size(this.members); + this.challengeCount = _.size(this.challenges); + return doc; } diff --git a/views/options/challenges.jade b/views/options/challenges.jade index 3593abab3d..879d31f8f4 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -5,7 +5,7 @@ script(type='text/ng-template', id='partials/options.challenges.detail.html') button.btn.btn-default(ng-click='challenge._locked = false') Edit li(ng-hide='challenge._locked') button.btn.btn-primary(ng-click='save(challenge)') Save - button.btn.btn-danger(ng-click='delete(challenge)') Delete + button.btn.btn-danger(ng-click='delete(challenge, $index)') Delete button.btn.btn-default(ng-click='challenge._locked=true') Cancel div(ng-hide='challenge._locked') @@ -32,13 +32,14 @@ script(type='text/ng-template', id='partials/options.challenges.html') | {{group.name}} li input(type='checkbox', ng-model='search.members') - | Subscribed (TODO) + | Particiapting in(TODO) li input(type='checkbox', ng-model='search.members') - | Available (TODO) + | Not participating in (TODO) .span10 // Creation form - a.btn.btn-success(ng-click='create()') Create Challenge + div(ng-hide='newChallenge') + button.btn.btn-success(ng-click='create()') Create Challenge .create-challenge-from(ng-if='newChallenge') form(ng-submit='save(newChallenge)') div @@ -54,7 +55,7 @@ script(type='text/ng-template', id='partials/options.challenges.html') .accordion-group(ng-repeat='challenge in challenges | filter:search', ng-init='challenge._locked=true') .accordion-heading ul.pull-right.challenge-accordion-header-specs - li {{challenge.members.length}} Subscribers + li {{challenge.memberCount}} Participants li(ng-show='challenge.prize') // prize table(ng-show='challenge.prize') @@ -64,13 +65,13 @@ script(type='text/ng-template', id='partials/options.challenges.html') span.Pet_Currency_Gem1x td Prize li - // subscribe / unsubscribe - a.btn.btn-small.btn-danger(ng-show='indexOf(challenge.members, user._id)', ng-click='clickUnsubscribe(challenge, $event)') + // leave / join + a.btn.btn-small.btn-danger(ng-show='challenge._isMember', ng-click='clickLeave(challenge, $event)') i.icon-ban-circle - | Unsubscribe - a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()') + | Leave + a.btn.btn-small.btn-success(ng-hide='challenge._isMember', ng-click='join(challenge)') i.icon-ok - | Subscribe + | Join a.accordion-toggle(ui-sref='options.challenges.detail({cid:challenge._id})') {{challenge.name}} (by {{challenge.leader.name}}) .accordion-body(ng-class='{collapse: !$stateParams.cid == challenge._id}') .accordion-inner(ng-if='$stateParams.cid == challenge._id') diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index c0c1c6557a..f88d9a238f 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -22,9 +22,9 @@ li(ng-repeat='task in obj[list.type+"s"]', class='task {{taskClasses(task, user. //challenges span(ng-if='task.challenge.id') span(ng-if='task.challenge.broken') - i.icon-bullhorn(style='background-color:red;', ng-click='task._edit=true', tooltip="Broken Challenge Link") + i.icon-bullhorn(style='background-color:red;', ng-click='task._editing = true', tooltip="Broken Challenge Link", tooltip-placement='right') span(ng-if='!task.challenge.broken') - i.icon-bullhorn(tooltip="Challenge Task") + i.icon-bullhorn(tooltip="Challenge") // delete a(ng-if='!task.challenge.id', ng-click='removeTask(obj[list.type+"s"], $index)', tooltip='Delete') i.icon-trash @@ -68,7 +68,7 @@ li(ng-repeat='task in obj[list.type+"s"]', class='task {{taskClasses(task, user. p a(ng-click='unlink(task, "keep")') Keep It |  |  - a(ng-click="remove(list, $index)") Remove It + a(ng-click="removeTask(obj[list.type+'s'], $index)") Remove It 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