diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56e37cfe5f..463face2ad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,5 +14,4 @@ HabitRPG uses [Trello](https://trello.com/b/EpoYEYod/habitrpg) to track feature # Contributing Code -* See [Contributing to HabitRPG](http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG) -* See [Tips](http://habitrpg.wikia.com/wiki/Tips) +* See [Contributing to HabitRPG](http://habitrpg.wikia.com/wiki/Contributing_to_HabitRPG#Coders_.28Web_.26_Mobile.29) diff --git a/Gruntfile.js b/Gruntfile.js index 0d7247aa83..d778230751 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -4,6 +4,8 @@ module.exports = function(grunt) { // Project configuration. grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + git_changelog: { minimal: { options: { @@ -56,7 +58,10 @@ module.exports = function(grunt) { build: { files: [ {expand: true, cwd: 'public/', src: 'favicon.ico', dest: 'build/'}, - {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/dist/spritesmith-*.png', dest: 'build/'} + {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/dist/spritesmith.png', dest: 'build/'}, + {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/img/sprites/backer-only/*.gif', dest: 'build/'}, + {expand: true, cwd: 'public/', src: 'bower_components/habitrpg-shared/img/sprites/npc_ian.gif', dest: 'build/'}, + {expand: true, cwd: 'public/', src: 'bower_components/bootstrap/dist/fonts/*', dest: 'build/'} ] } }, @@ -69,16 +74,18 @@ module.exports = function(grunt) { }, src: [ 'build/*.js', 'build/*.css', 'build/favicon.ico', - 'build/bower_components/bootstrap/docs/assets/css/*.css', - 'build/bower_components/habitrpg-shared/dist/*.css' + 'build/bower_components/habitrpg-shared/dist/*.png', + 'build/bower_components/habitrpg-shared/img/sprites/backer-only/*.gif', + 'build/bower_components/habitrpg-shared/img/sprites/npc_ian.gif', + 'build/bower_components/bootstrap/dist/fonts/*' ], - dest: 'make-sure-i-do-not-exist' + dest: 'build/*.css' } }, - nodemon: { + nodemon: { dev: { - ignoredFiles: ['public/*', 'Gruntfile.js', 'CHANGELOG.md', 'views/*', 'build/*', '.idea*', '.git*', '*.log'] + script: '<%= pkg.main %>' } }, @@ -106,27 +113,35 @@ module.exports = function(grunt) { var files = grunt.file.readJSON('./public/manifest.json'); var uglify = {}; var cssmin = {}; + _.each(files, function(val, key){ + var js = uglify['build/' + key + '.js'] = []; + _.each(files[key]['js'], function(val){ js.push('public/' + val); }); + + var css = cssmin['build/' + key + '.css'] = []; + _.each(files[key]['css'], function(val){ - if(val == 'app.css' || val == 'static.css'){ - cssmin['build/' + val] = ['build/' + val] - }else{ - cssmin['build/' + val] = ['public/' + val] - } + var path = (val == 'app.css' || val == 'static.css') ? 'build/' : 'public/'; + css.push(path + val) }); + }); + grunt.config.set('uglify.build.files', uglify); - grunt.config.set('uglify.build.options', {compress: false}) + grunt.config.set('uglify.build.options', {compress: false}); + grunt.config.set('cssmin.build.files', cssmin); + // Rewrite urls to relative path + grunt.config.set('cssmin.build.options', {'target': 'public/css/whatever-css.css'}); }); // Register tasks. grunt.registerTask('build:prod', ['loadManifestFiles', 'clean:build', 'uglify', 'stylus', 'cssmin', 'copy:build', 'hashres']); - grunt.registerTask('build:dev', ['loadManifestFiles', 'clean:build', 'stylus', 'cssmin', 'copy:build']); + grunt.registerTask('build:dev', ['stylus']); grunt.registerTask('run:dev', [ 'build:dev', 'concurrent' ]); diff --git a/bower.json b/bower.json index e9f2e6ae37..a6de245fda 100644 --- a/bower.json +++ b/bower.json @@ -17,9 +17,9 @@ "jquery": "~2.1.0", "jquery.cookie": "~1.4.0", "jquery-ui": "~1.10.3", - "angular": "~1.2.11", - "angular-sanitize": "~1.2.11", - "angular-resource": "~1.2.11", + "angular": "~1.2.12", + "angular-sanitize": "~1.2.12", + "angular-resource": "~1.2.12", "angular-ui-utils": "~0.1.0", "angular-ui-select2": "https://github.com/angular-ui/ui-select2.git#4469fd7a32eba2e28d5e3dd319b49e1ad090b167", "angular-bootstrap": "~0.10.0", @@ -36,13 +36,12 @@ "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master", "Angular-At-Directive": "git://github.com/snicker/Angular-At-Directive#master", "js-emoji": "git://github.com/snicker/js-emoji#master", - "gemoji": "git://github.com/github/gemoji", "sticky": "*", "swagger-ui": "https://github.com/wordnik/swagger-ui.git#105c516f5f055b140e935e9cfe0c36f841921dff", "ngInfiniteScroll": "~1.0.0", "jquery-colorbox": "~1.4.36" }, "devDependencies": { - "angular-mocks": "~1.2.11" + "angular-mocks": "~1.2.12" } } diff --git a/migrations/contribs_plan.js b/migrations/contribs_plan.js new file mode 100644 index 0000000000..a08bd93f76 --- /dev/null +++ b/migrations/contribs_plan.js @@ -0,0 +1,23 @@ +// Give contrib.level 7+ free subscription for life +db.users.update( + + { + 'contributor.level':{$gte:7}, + 'purchased.plan.customerId':null + }, + + { + $set: { + 'purchased.plan':{ + planId: 'basic', + customerId: 'habitrpg', + dateCreated: new Date, + dateUpdated: new Date, + gemsBought: 0 + } + } + }, + + {multi:true} + +) \ No newline at end of file diff --git a/public/css/alerts.styl b/public/css/alerts.styl index 1520f695f8..40a7b8910b 100644 --- a/public/css/alerts.styl +++ b/public/css/alerts.styl @@ -33,6 +33,10 @@ alert-sm-mixin() .alert alert-sm-mixin() + .notification-icon + transform: scale(0.8) + + // variables gpColor = #FBEFC0 @@ -95,35 +99,6 @@ borderDarken = 20% border-color: darken(dropColor,borderDarken) color: dropText -// alert icons - -.icon-gold - background: url("/bower_components/habitrpg-shared/img/coin_single_gold.png") no-repeat - background-position: center center - background-size: 18px - width: 14px - height: 14px - display: inline-block - vertical-align: middle - -.icon-silver - background: url("/bower_components/habitrpg-shared/img/coin_single_silver.png") no-repeat - background-position: center center - background-size: 18px - width: 14px - height: 14px - display: inline-block - vertical-align: middle - -.icon-death - background: url("/bower_components/habitrpg-shared/img/sprites/dead.png") no-repeat - background-position: center center - background-size: 14px - width: 14px - height: 14px - display: inline-block - vertical-align: middle - .undo-button z-index: 3000 position:absolute diff --git a/public/css/index.styl b/public/css/index.styl index 6d00b3e041..c10b3b12c9 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -118,6 +118,10 @@ html, body float: left padding: 15px + &.herobox + padding-right: 0px + padding-left: 0px + .death-modal // a .modal-body margin: 10px @@ -163,3 +167,19 @@ a.label background-color: #428bca .badge-success background-color: #47a447 + +.white, .white a + color: #fff !important + +.collapse-toolbar + position:absolute + right:0px + top:0px + width:30px + height:25px + background-color:#222 + z-index:999 + .glyphicon + color:white + padding-left:7px + padding-top:5px diff --git a/public/css/npcs.styl b/public/css/npcs.styl index 48493536b3..4f55489d5c 100644 --- a/public/css/npcs.styl +++ b/public/css/npcs.styl @@ -4,15 +4,15 @@ .npc_matt margin-bottom: 20px +.toolbar-bailey-container + width:55px + position:relative .npc_bailey float:left .npc_bailey_head - //height: 30px !important - height: 50px !important position: absolute - //top: 117px // 147 (header-height) - 30 (bailey height) - top: 97px - right: 50px + top: 4px + height:45px !important cursor: pointer // Tour (Justin) diff --git a/public/js/app.js b/public/js/app.js index 57fb7a25f3..ef55bb10d4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -114,8 +114,12 @@ window.habitrpg = angular.module('habitrpg', .state('options.social.guilds.detail', { url: '/:gid', templateUrl: 'partials/options.social.guilds.detail.html', - controller: ['$scope', 'Groups', '$stateParams', function($scope, Groups, $stateParams){ - $scope.group = Groups.Group.get({gid:$stateParams.gid}); + controller: ['$scope', 'Groups', '$stateParams', + function($scope, Groups, $stateParams){ + Groups.Group.get({gid:$stateParams.gid}, function(group){ + $scope.group = group; + Groups.seenMessage(group._id); + }); }] }) diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index 1c5bd00958..c0ba02c5a1 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -319,6 +319,9 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' $scope.text = 'Party'; $scope.group = $rootScope.party = Groups.party(); $scope.newGroup = new Groups.Group({type:'party'}); + + Groups.seenMessage($scope.group._id); + $scope.create = function(group){ group.$save(function(newGroup){ $scope.group = newGroup; diff --git a/public/js/controllers/notificationCtrl.js b/public/js/controllers/notificationCtrl.js index 8633c16fc5..82c42af8a7 100644 --- a/public/js/controllers/notificationCtrl.js +++ b/public/js/controllers/notificationCtrl.js @@ -94,7 +94,6 @@ habitrpg.controller('NotificationCtrl', $rootScope.$watch('user.items.' + watched, function(before, after){ if (after == before) return; if (+after < +before) { - Notification.death(); //don't want to day "lost a head" if (watched === 'head') watched = 'helm'; Notification.text('Lost GP, 1 LVL, ' + watched); @@ -116,12 +115,6 @@ habitrpg.controller('NotificationCtrl', } }); - // Math updates modal - $rootScope.$watch('!user.flags.mathUpdates', function(after, before){ - if (after == before || after != true) return; - $rootScope.openModal('mathUpdates'); - }); - // Completed quest modal $rootScope.$watch('user.party.quest.completed', function(after, before){ if (after == before || after != true) return; diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index d9d56efe9e..508afbdf33 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -26,6 +26,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $rootScope.Shared = Shared; $rootScope.Content = Content; $rootScope.env = window.env; + $rootScope.Math = Math; // Angular UI Router $rootScope.$state = $state; @@ -198,6 +199,11 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ */ $scope.castStart = function(spell) { if (User.user.stats.mp < spell.mana) return Notification.text(window.env.t('notEnoughMana')); + + // Temporary for valentine's day, remove after event + if (spell.key == 'valentine' && User.user.stats.gp < spell.value) + return Notification.text('Not enough gold.'); + $rootScope.applyingAction = true; $scope.spell = spell; if (spell.target == 'self') { diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 1f72b4a648..db33d73de9 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -5,8 +5,8 @@ */ angular.module('groupServices', ['ngResource']). - factory('Groups', ['API_URL', '$resource', '$q', - function(API_URL, $resource, $q) { + factory('Groups', ['API_URL', '$resource', '$q', '$http', 'User', + function(API_URL, $resource, $q, $http, User) { var Group = $resource(API_URL + '/api/v2/groups/:gid', {gid:'@_id', messageId: '@_messageId'}, { @@ -44,6 +44,12 @@ angular.module('groupServices', ['ngResource']). return tavern; }, + // On enter, set chat message to "seen" + seenMessage: function(gid){ + $http.post('/api/v2/groups/'+gid+'/chat/seen'); + if (User.user.newMessages) User.user.newMessages[gid] = false; + }, + Group: Group } } diff --git a/public/js/services/notificationServices.js b/public/js/services/notificationServices.js index 02f92bbc83..c67770e8f0 100644 --- a/public/js/services/notificationServices.js +++ b/public/js/services/notificationServices.js @@ -25,11 +25,11 @@ angular.module("notificationServices", []) gold = Math.floor(absolute); silver = Math.floor((absolute - gold) * 100); if (gold && silver > 0) { - return "" + gold + " " + silver + " "; + return "" + gold + " " + silver + " "; } else if (gold > 0) { - return "" + gold + " "; + return "" + gold + " "; } else if (silver > 0) { - return "" + silver + " "; + return "" + silver + " "; } }; @@ -60,9 +60,6 @@ angular.module("notificationServices", []) lvl: function(){ growl(' ' + window.env.t('levelUp'), 'lvl'); }, - death: function(){ - growl(" " + window.env.t('respawn'), "death"); - }, error: function(error){ growl(" " + error, "danger"); }, diff --git a/public/manifest.json b/public/manifest.json index ce7e2b6e8c..85e39cd970 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "app": { "js": [ - "bower_components/jquery/jquery.js", + "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery.cookie/jquery.cookie.js", "bower_components/bootstrap-growl/jquery.bootstrap-growl.js", "bower_components/bootstrap-tour/build/js/bootstrap-tour.js", @@ -72,7 +72,7 @@ }, "static": { "js": [ - "bower_components/jquery/jquery.js", + "bower_components/jquery/dist/jquery.min.js", "bower_components/habitrpg-shared/dist/habitrpg-shared.js", "bower_components/angular/angular.js", "bower_components/angular-ui/build/angular-ui.js", diff --git a/src/controllers/groups.js b/src/controllers/groups.js index cfdaa2d179..4bdde2b83a 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -249,6 +249,14 @@ api.deleteChatMessage = function(req, res){ }); } +api.seenMessage = function(req,res,next){ + // Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save + var update = {$set:{}}; + update['$set']['newMessages.'+req.params.gid+'.value'] = false; + User.update({_id:req.headers['x-api-user'], apiToken:req.headers['x-api-key']},update).exec(); + res.send(200); +} + api.likeChatMessage = function(req, res, next) { var user = res.locals.user; var group = res.locals.group; diff --git a/src/controllers/user.js b/src/controllers/user.js index 5556a25093..b8979639c4 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -89,10 +89,25 @@ api.score = function(req, res, next) { delta: delta, _tmp: user._tmp }, saved.toJSON().stats)); - }); - // if it's a challenge task, sync the score - user.syncScoreToChallenge(task, delta); + // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response + // and the user doesn't care what happens back there + if (!task.challenge || !task.challenge.id || task.challenge.broken) return; + if (task.type == 'reward') return; // we don't want to update the reward GP cost + Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal){ + if (err) return next(err); + if (!chal) { + task.challenge.broken = 'CHALLENGE_DELETED'; + return user.save(); + } + var t = chal.tasks[task.id]; + if (!t) return chal.syncToUser(user); // this task was removed from the challenge, notify user + t.value += delta; + if (t.type == 'habit' || t.type == 'daily') + t.history.push({value: t.value, date: +new Date}); + chal.save(); + }); + }); }; /** @@ -409,7 +424,7 @@ api.cast = function(req, res, next) { case 'user': async.waterfall([ function(cb){ - Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members', 'profile.name stats achievements').exec(cb); + Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members', 'profile.name stats achievements items.special').exec(cb); }, function(group, cb) { // Solo player? let's just create a faux group for simpler code diff --git a/src/middleware.js b/src/middleware.js index 30e95ca5c6..4894b586d1 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -122,22 +122,21 @@ var getManifestFiles = function(page){ if(!files) throw new Error("Page not found!"); - var css = ''; - - _.each(files.css, function(file){ - css += ''; - }); + var code = ''; if(nconf.get('NODE_ENV') === 'production'){ - return css + ''; + code += ''; + code += ''; }else{ - var results = css; - _.each(files.js, function(file){ - results += ''; + _.each(files.css, function(file){ + code += ''; + }); + _.each(files.js, function(file){ + code += ''; }); - return results; } - + + return code; } // Translations diff --git a/src/models/group.js b/src/models/group.js index f3b3cad8e5..5d80c0baf4 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -106,6 +106,17 @@ GroupSchema.methods.sendChat = function(message, user){ } group.chat.unshift(message); group.chat.splice(200); + + // Kick off chat notifications in the background. + var lastSeenUpdate = {$set:{}, $inc:{_v:1}}; + lastSeenUpdate['$set']['newMessages.'+group._id] = {name:group.name,value:true}; + if (group._id == 'habitrpg') { + // TODO For Tavern, only notify them if their name was mentioned + // var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names? + // User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec(); + } else { + mongoose.model('User').update({_id:{$in:group.members, $ne: user ? user._id : ''}},lastSeenUpdate,{multi:true}).exec(); + } } var cleanQuestProgress = function(merge){ @@ -164,13 +175,13 @@ GroupSchema.methods.finishQuest = function(quest, cb) { } // FIXME this is a temporary measure, we need to remove quests from users when they traverse parties -function isOnQuest(user,group){ - return group && user.party.quest.key && user.party.quest.key == group.quest.key; +function isOnQuest(user,progress,group){ + return group && progress && user.party.quest.key && user.party.quest.key == group.quest.key; } GroupSchema.statics.collectQuest = function(user, progress, cb) { this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){ - if (!isOnQuest(user,group)) return cb(null); + if (!isOnQuest(user,progress,group)) return cb(null); var quest = shared.content.quests[group.quest.key]; _.each(progress.collect,function(v,k){ @@ -204,7 +215,7 @@ GroupSchema.statics.collectQuest = function(user, progress, cb) { GroupSchema.statics.bossQuest = function(user, progress, cb) { this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){ - if (!isOnQuest(user,group)) return cb(null); + if (!isOnQuest(user,progress,group)) return cb(null); var quest = shared.content.quests[group.quest.key]; if (!progress || !quest) return cb(null); // FIXME why is this ever happening, progress should be defined at this point var down = progress.down * quest.boss.str; // multiply by boss strength diff --git a/src/models/user.js b/src/models/user.js index 2d81658e0f..9c1c5ba0cc 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -42,7 +42,8 @@ var UserSchema = new Schema({ rebirths: Number, rebirthLevel: Number, perfect: Number, - habitBirthday: Boolean + habitBirthday: Boolean, + valentine: Number }, auth: { facebook: Schema.Types.Mixed, @@ -136,7 +137,9 @@ var UserSchema = new Schema({ }, special:{ - snowball: {type: Number, 'default': 0} + snowball: {type: Number, 'default': 0}, + valentine: Number, + valentineReceived: Array // array of strings, by sender name }, // -------------- Animals ------------------- @@ -207,9 +210,11 @@ var UserSchema = new Schema({ lastCron: {type: Date, 'default': Date.now}, + // {GROUP_ID: Boolean}, represents whether they have unseen chat messages + newMessages: {type: Schema.Types.Mixed, 'default': {}}, + party: { // id // FIXME can we use a populated doc instead of fetching party separate from user? - lastMessageSeen: String, order: {type:String, 'default':'level'}, quest: { key: String, @@ -245,7 +250,8 @@ var UserSchema = new Schema({ disableClasses: {type: Boolean, 'default': false}, newTaskEdit: {type: Boolean, 'default': false}, tagsCollapsed: {type: Boolean, 'default': false}, - advancedCollapsed: {type: Boolean, 'default': false} + advancedCollapsed: {type: Boolean, 'default': false}, + toolbarCollapsed: {type:Boolean, 'default':false} }, profile: { blurb: String, @@ -367,22 +373,6 @@ UserSchema.pre('save', function(next) { next(); }); -UserSchema.methods.syncScoreToChallenge = function(task, delta){ - if (!task.challenge || !task.challenge.id || task.challenge.broken) return; - if (task.type == 'reward') return; // we don't want to update the reward GP cost - var self = this; - Challenge.findById(task.challenge.id, function(err, chal){ - if (err) throw err; - var t = chal.tasks[task.id]; - if (!t) return chal.syncToUser(self); // this task was removed from the challenge, notify user - t.value += delta; - if (t.type == 'habit' || t.type == 'daily') { - t.history.push({value: t.value, date: +new Date}); - } - chal.save(); - }); -} - UserSchema.methods.unlink = function(options, cb) { var cid = options.cid, keep = options.keep, tid = options.tid; var self = this; diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 513fb7e312..db5a0010f0 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -488,6 +488,17 @@ module.exports = (swagger, v2) -> middleware: [auth.auth, groups.attachGroup] action: groups.postChat + # placing before route below, so that if !=='seen' it goes to next() + "/groups/{gid}/chat/seen": + spec: + method: 'POST' + description: "Flag chat messages for a particular group as seen" + parameters: [ + path 'gid','Group id','string' + ] + middleware: [] + action: groups.seenMessage + "/groups/{gid}/chat/{messageId}": spec: method: 'DELETE' diff --git a/src/server.js b/src/server.js index 6cefab8db0..bef7642207 100644 --- a/src/server.js +++ b/src/server.js @@ -116,7 +116,9 @@ if (cluster.isMaster && (isDev || isProd)) { app.use(app.router); var maxAge = isProd ? 31536000000 : 0; + // Cache emojis without copying them to build, they are too many app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge })); + app.use('/bower_components/habitrpg-shared/img/emoji/unicode', express['static'](path.join(__dirname, "/../public/bower_components/habitrpg-shared/img/emoji/unicode"), { maxAge: maxAge })); app.use(express['static'](path.join(__dirname, "/../public"))); // Custom Directives diff --git a/test/api.mocha.js b/test/api.mocha.js index 9b69fd461e..3f4f20d6f2 100644 --- a/test/api.mocha.js +++ b/test/api.mocha.js @@ -253,6 +253,33 @@ describe('API', function () { }); }); + it('Challenge deleted, breaks task link', function (done) { + var itThis = this; + request.del(baseURL + "/challenges/" + challenge._id).end(function (res) { + User.findById(user._id, function(err,user){ + var len = user.dailys.length- 1, + daily = user.dailys[user.dailys.length-1]; + expect(daily.challenge.broken).to.be('CHALLENGE_DELETED'); + + // Now let's handle if challenge was deleted, but didn't get to update all the users (an error) + var unset = {$unset:{}}; + unset['$unset']['dailys.' + len + '.challenge.broken'] = 1; + User.findByIdAndUpdate(user._id,unset,function(err,user){ + expect(err).to.not.be.ok(); + expect(user.dailys[len].challenge.broken).to.not.be.ok(); + request.post(baseURL + "/user/tasks/" + daily.id + "/up").end(function (res) { + setTimeout(function(){ + User.findById(user._id,function(err,user){ + expect(user.dailys[len].challenge.broken).to.be('CHALLENGE_DELETED'); + done() + }) + }, 100); // we need to wait for challenge to update user, it's a background job for perf reasons + }) + }) + }) + }) + }); + it('Admin creates a challenge', function (done) { User.findByIdAndUpdate(_id, {$set:{'contributor.admin':true}}, function (err,_user) { expect(err).to.not.be.ok(); diff --git a/views/index.jade b/views/index.jade index aedb605b44..85f6c5d642 100644 --- a/views/index.jade +++ b/views/index.jade @@ -20,19 +20,15 @@ html(ng-app="habitrpg", ng-controller="RootCtrl", ng-class='{"applying-action":a body(ng-cloak, ng-controller='GroupsCtrl') + include ./shared/header/menu include ./shared/modals/index include ./shared/header/header - include ./shared/tasks/lists include ./main/index include ./options/index #notification-area(ng-controller='NotificationCtrl') #wrap.container-fluid - //if they hide the header, we still need user-menu visible - div(ng-if='user.preferences.hideHeader') - include ./shared/header/menu - .row .col-md-12.exp-chart(ng-show='charts.exp') #main(ui-view) diff --git a/views/main/filters.jade b/views/main/filters.jade index 923d61e2a6..e356e552e5 100644 --- a/views/main/filters.jade +++ b/views/main/filters.jade @@ -21,7 +21,7 @@ form.form-inline(ng-show='_editing', ng-submit='createTag(_newTag)') .input-group input.form-control.input-sm(type='text', ng-model='_newTag', placeholder=env.t('newTag')) - span.input-group-addon(type='submit') + span.input-group-addon(ng-click='createTag(_newTag)') =env.t('add') //