challenges: lots of misc. bug fixes

This commit is contained in:
Tyler Renelle
2013-10-29 20:37:22 -07:00
parent 6e71a76e22
commit d116ac7b81
6 changed files with 100 additions and 39 deletions

View File

@@ -1,7 +1,7 @@
"use strict"; "use strict";
habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notification', '$compile', 'Groups', habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notification', '$compile', 'Groups', '$state',
function($scope, User, Challenges, Notification, $compile, Groups) { function($scope, User, Challenges, Notification, $compile, Groups, $state) {
// FIXME get this from cache // FIXME get this from cache
Groups.Group.query(function(groups){ Groups.Group.query(function(groups){
@@ -20,7 +20,7 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica
* Create * Create
*/ */
$scope.create = function() { $scope.create = function() {
$scope.newChallenge = new Challenges.Challenge({ $scope.obj = $scope.newChallenge = new Challenges.Challenge({
name: '', name: '',
description: '', description: '',
habits: [], habits: [],
@@ -40,11 +40,11 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica
$scope.save = function(challenge) { $scope.save = function(challenge) {
if (!challenge.group) return alert('Please select group'); if (!challenge.group) return alert('Please select group');
var isNew = !challenge._id; var isNew = !challenge._id;
challenge.$save(function(){ challenge.$save(function(_challenge){
if (isNew) { if (isNew) {
Notification.text('Challenge Created'); Notification.text('Challenge Created');
$scope.discard(); $scope.discard();
Challenges.Challenge.query(); $scope.challenges.unshift(_challenge);
} else { } else {
// TODO figure out a more elegant way about this // TODO figure out a more elegant way about this
//challenge._editing = false; //challenge._editing = false;
@@ -64,9 +64,13 @@ habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notifica
/** /**
* Delete * Delete
*/ */
$scope["delete"] = function(challenge) { $scope["delete"] = function(challenge, $index) {
if (confirm("Delete challenge, are you sure?") !== true) return; 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') { if (keep == 'cancel') {
$scope.selectedChal = undefined; $scope.selectedChal = undefined;
} else { } else {
$scope.selectedChal.$leave({keep:keep}); $scope.selectedChal.$leave({keep:keep}, function(){
User.log({});
});
} }
$scope.popoverEl.popover('destroy'); $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.selectedChal = chal;
$scope.popoverEl = $($event.target); $scope.popoverEl = $($event.target);
var html = $compile( var html = $compile(
'<a ng-controller="ChallengesCtrl" ng-click="unsubscribe(\'remove-all\')">Remove Tasks</a><br/>\n<a ng-click="unsubscribe(\'keep-all\')">Keep Tasks</a><br/>\n<a ng-click="unsubscribe(\'cancel\')">Cancel</a><br/>' '<a ng-controller="ChallengesCtrl" ng-click="leave(\'remove-all\')">Remove Tasks</a><br/>\n<a ng-click="leave(\'keep-all\')">Keep Tasks</a><br/>\n<a ng-click="leave(\'cancel\')">Cancel</a><br/>'
)($scope); )($scope);
$scope.popoverEl.popover('destroy').popover({ $scope.popoverEl.popover('destroy').popover({
html: true, html: true,
placement: 'top', placement: 'top',
trigger: 'manual', trigger: 'manual',
title: 'Unsubscribe From Challenge And:', title: 'Leave challenge and...',
content: html content: html
}).popover('show'); }).popover('show');
} }

View File

@@ -26,10 +26,15 @@ api.list = function(req, res) {
{group: 'habitrpg'} {group: 'habitrpg'}
] ]
}) })
.select('name description memberCount groups') .select('name description memberCount groups members')
.populate('groups', '_id name') .populate('groups', '_id name')
.exec(function(err, challenges){ .exec(function(err, challenges){
if (err) return res.json(500,{err:err}); 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); res.json(challenges);
}); });
} }
@@ -43,6 +48,8 @@ api.get = function(req, res) {
if(err) return res.json(500, {err:err}); if(err) return res.json(500, {err:err});
// slim down the return members' tasks to only the ones in the challenge // slim down the return members' tasks to only the ones in the challenge
_.each(challenge.members, function(member){ _.each(challenge.members, function(member){
if (member._id == user._id)
challenge._isMember = true;
_.each(['habits', 'dailys', 'todos', 'rewards'], function(type){ _.each(['habits', 'dailys', 'todos', 'rewards'], function(type){
member[type] = _.where(member[type], function(task){ member[type] = _.where(member[type], function(task){
return task.challenge && task.challenge.id == challenge._id; return task.challenge && task.challenge.id == challenge._id;
@@ -58,9 +65,8 @@ api.create = function(req, res){
// FIXME sanitize // FIXME sanitize
var challenge = new Challenge(req.body); var challenge = new Challenge(req.body);
challenge.save(function(err, saved){ 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}); if (err) return res.json(500, {err:err});
Group.findByIdAndUpdate(saved.group, {$addToSet:{challenges:saved._id}}) // fixme error-check
res.json(saved); res.json(saved);
}); });
} }
@@ -118,19 +124,32 @@ api.update = function(req, res){
// DELETE // DELETE
api['delete'] = function(req, res){ api['delete'] = function(req, res){
Challenge.findOneAndRemove({_id:req.params.cid}, function(err, removed){ var removed;
if (err) return res.json(500, {err: err}); async.waterfall([
User.find({_id:{$in: removed.members}}, function(err, users){ function(cb){
if (err) throw err; 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(users, function(user){
_.each(user.tasks, function(task){ _.each(user.tasks, function(task){
if (task.challenge && task.challenge.id == removed._id) { if (task.challenge && task.challenge.id == removed._id) {
task.challenge.broken = 'CHALLENGE_DELETED'; 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){ ], function(err, result){
if(err) return res.json(500,{err:err}); if(err) return res.json(500,{err:err});
result._isMember = true;
res.json(result); res.json(result);
}); });
} }
@@ -206,7 +226,7 @@ api.join = function(req, res){
function unlink(user, cid, keep, tid) { function unlink(user, cid, keep, tid) {
switch (keep) { switch (keep) {
case 'keep': case 'keep':
delete user.tasks[tid].challenge; user.tasks[tid].challenge = {};
break; break;
case 'remove': case 'remove':
user[user.tasks[tid].type+'s'].id(tid).remove(); user[user.tasks[tid].type+'s'].id(tid).remove();
@@ -214,7 +234,7 @@ function unlink(user, cid, keep, tid) {
case 'keep-all': case 'keep-all':
_.each(user.tasks, function(t){ _.each(user.tasks, function(t){
if (t.challenge && t.challenge.id == cid) { if (t.challenge && t.challenge.id == cid) {
delete t.challenge; t.challenge = {};
} }
}); });
break; break;
@@ -249,6 +269,7 @@ api.leave = function(req, res){
} }
], function(err, result){ ], function(err, result){
if(err) return res.json(500,{err:err}); if(err) return res.json(500,{err:err});
result._isMember = false;
res.json(result); res.json(result);
}); });
} }
@@ -264,6 +285,10 @@ api.unlink = function(req, res, next) {
if (!req.query.keep) if (!req.query.keep)
return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'}); 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); unlink(user, cid, req.query.keep, tid);
user.markModified('habits');
user.markModified('dailys');
user.markModified('todos');
user.markModified('rewards');
user.save(function(err, saved){ user.save(function(err, saved){
if (err) return res.json(500,{err:err}); if (err) return res.json(500,{err:err});
res.send(200); res.send(200);

View File

@@ -3,6 +3,7 @@ var Schema = mongoose.Schema;
var helpers = require('habitrpg-shared/script/helpers'); var helpers = require('habitrpg-shared/script/helpers');
var _ = require('lodash'); var _ = require('lodash');
var TaskSchema = require('./task').schema; var TaskSchema = require('./task').schema;
var Group = require('./group').model;
var ChallengeSchema = new Schema({ var ChallengeSchema = new Schema({
_id: {type: String, 'default': helpers.uuid}, _id: {type: String, 'default': helpers.uuid},
@@ -20,7 +21,7 @@ var ChallengeSchema = new Schema({
//}, //},
timestamp: {type: Date, 'default': Date.now}, timestamp: {type: Date, 'default': Date.now},
members: [{type: String, ref: 'User'}], members: [{type: String, ref: 'User'}],
memberCount: [{type: Number, 'default': 0}] memberCount: {type: Number, 'default': 0}
}); });
ChallengeSchema.virtual('tasks').get(function () { ChallengeSchema.virtual('tasks').get(function () {
@@ -29,10 +30,19 @@ ChallengeSchema.virtual('tasks').get(function () {
return tasks; 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){ ChallengeSchema.pre('save', function(next){
this.memberCount = _.size(this.members); 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.schema = ChallengeSchema;
module.exports.model = mongoose.model("Challenge", ChallengeSchema); module.exports.model = mongoose.model("Challenge", ChallengeSchema);

View File

@@ -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){ GroupSchema.pre('save', function(next){
removeDuplicates(this); removeDuplicates(this);
this.memberCount = _.size(this.members); this.memberCount = _.size(this.members);
@@ -69,6 +71,11 @@ GroupSchema.methods.toJSON = function(){
var doc = this.toObject(); var doc = this.toObject();
removeDuplicates(doc); removeDuplicates(doc);
doc._isMember = this._isMember; doc._isMember = this._isMember;
// @see pre('save') comment above
this.memberCount = _.size(this.members);
this.challengeCount = _.size(this.challenges);
return doc; return doc;
} }

View File

@@ -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 button.btn.btn-default(ng-click='challenge._locked = false') Edit
li(ng-hide='challenge._locked') li(ng-hide='challenge._locked')
button.btn.btn-primary(ng-click='save(challenge)') Save 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 button.btn.btn-default(ng-click='challenge._locked=true') Cancel
div(ng-hide='challenge._locked') div(ng-hide='challenge._locked')
@@ -32,13 +32,14 @@ script(type='text/ng-template', id='partials/options.challenges.html')
| {{group.name}} | {{group.name}}
li li
input(type='checkbox', ng-model='search.members') input(type='checkbox', ng-model='search.members')
| Subscribed (TODO) | Particiapting in(TODO)
li li
input(type='checkbox', ng-model='search.members') input(type='checkbox', ng-model='search.members')
| Available (TODO) | Not participating in (TODO)
.span10 .span10
// Creation form // 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') .create-challenge-from(ng-if='newChallenge')
form(ng-submit='save(newChallenge)') form(ng-submit='save(newChallenge)')
div 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-group(ng-repeat='challenge in challenges | filter:search', ng-init='challenge._locked=true')
.accordion-heading .accordion-heading
ul.pull-right.challenge-accordion-header-specs ul.pull-right.challenge-accordion-header-specs
li {{challenge.members.length}} Subscribers li {{challenge.memberCount}} Participants
li(ng-show='challenge.prize') li(ng-show='challenge.prize')
// prize // prize
table(ng-show='challenge.prize') table(ng-show='challenge.prize')
@@ -64,13 +65,13 @@ script(type='text/ng-template', id='partials/options.challenges.html')
span.Pet_Currency_Gem1x span.Pet_Currency_Gem1x
td Prize td Prize
li li
// subscribe / unsubscribe // leave / join
a.btn.btn-small.btn-danger(ng-show='indexOf(challenge.members, user._id)', ng-click='clickUnsubscribe(challenge, $event)') a.btn.btn-small.btn-danger(ng-show='challenge._isMember', ng-click='clickLeave(challenge, $event)')
i.icon-ban-circle i.icon-ban-circle
| Unsubscribe | Leave
a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()') a.btn.btn-small.btn-success(ng-hide='challenge._isMember', ng-click='join(challenge)')
i.icon-ok i.icon-ok
| Subscribe | Join
a.accordion-toggle(ui-sref='options.challenges.detail({cid:challenge._id})') {{challenge.name}} (by {{challenge.leader.name}}) 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-body(ng-class='{collapse: !$stateParams.cid == challenge._id}')
.accordion-inner(ng-if='$stateParams.cid == challenge._id') .accordion-inner(ng-if='$stateParams.cid == challenge._id')

View File

@@ -22,9 +22,9 @@ li(ng-repeat='task in obj[list.type+"s"]', class='task {{taskClasses(task, user.
//challenges //challenges
span(ng-if='task.challenge.id') span(ng-if='task.challenge.id')
span(ng-if='task.challenge.broken') 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') span(ng-if='!task.challenge.broken')
i.icon-bullhorn(tooltip="Challenge Task") i.icon-bullhorn(tooltip="Challenge")
// delete // delete
a(ng-if='!task.challenge.id', ng-click='removeTask(obj[list.type+"s"], $index)', tooltip='Delete') a(ng-if='!task.challenge.id', ng-click='removeTask(obj[list.type+"s"], $index)', tooltip='Delete')
i.icon-trash i.icon-trash
@@ -68,7 +68,7 @@ li(ng-repeat='task in obj[list.type+"s"]', class='task {{taskClasses(task, user.
p p
a(ng-click='unlink(task, "keep")') Keep It a(ng-click='unlink(task, "keep")') Keep It
| &nbsp;|&nbsp; | &nbsp;|&nbsp;
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"') 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 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 p