From e81193ef28effd336c16feaed7a31efa5c6474bb Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 13 Oct 2013 16:42:20 -0700 Subject: [PATCH 01/15] challenges: pull in updates from the old 0.5 branch --- archive/derby_controllers/challenges.coffee | 312 +++++++++++++++----- views/options/challenges.html | 196 +++++------- views/options/groups/group.jade | 42 +-- views/tasks/task.jade | 39 ++- 4 files changed, 355 insertions(+), 234 deletions(-) diff --git a/archive/derby_controllers/challenges.coffee b/archive/derby_controllers/challenges.coffee index 5d61133a7c..d89ebef890 100644 --- a/archive/derby_controllers/challenges.coffee +++ b/archive/derby_controllers/challenges.coffee @@ -1,83 +1,239 @@ _ = require 'lodash' -helpers = require 'habitrpg-shared/script/helpers' +{helpers} = require 'habitrpg-shared' +async = require 'async' -module.exports.app = (appExports, model) -> - browser = require './browser' - user = model.at '_user' +module.exports.app = (app) -> - $('#profile-challenges-tab-link').on 'show', (e) -> - _.each model.get('groups'), (g) -> - _.each g.challenges, (chal) -> - _.each ['habit','daily','todo'], (type) -> - _.each chal["#{type}s"], (task) -> - _.each chal.users, (member) -> - if (history = member?["#{type}s"]?[task.id]?.history) and !!history - data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) - options = - backgroundColor: { fill:'transparent' } - width: 150 - height: 50 - chartArea: width: '80%', height: '80%' - axisTitlePosition: 'none' - legend: position: 'bottom' - hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines... - vAxis: gridlines: color: 'transparent' - chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] - chart.draw(data, options) - - - appExports.challengeCreate = (e,el) -> - [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] - model.set '_challenge.new', - name: '' - habits: [] - dailys: [] - todos: [] - rewards: [] - id: model.id() - uid: user.get('id') - user: helpers.username(model.get('_user.auth'), model.get('_user.profile.name')) - group: {type, id:gid} - timestamp: +new Date - - appExports.challengeSave = -> - gid = model.get('_challenge.new.group.id') - model.unshift "groups.#{gid}.challenges", model.get('_challenge.new'), -> - browser.growlNotification('Challenge Created','success') - challengeDiscard() - - appExports.toggleChallengeEdit = (e, el) -> - path = "_editing.challenges.#{$(el).attr('data-id')}" - model.set path, !model.get(path) - - appExports.challengeDiscard = challengeDiscard = -> model.del '_challenge.new' - - appExports.challengeSubscribe = (e) -> - chal = e.get() - - # Add challenge name as a tag for user - tags = user.get('tags') - unless tags and _.find(tags,{id: chal.id}) - model.push '_user.tags', {id: chal.id, name: chal.name, challenge: true} - - tags = {}; tags[chal.id] = true - # Add all challenge's tasks to user's tasks - userChallenges = user.get('challenges') - user.unshift('challenges', chal.id) unless userChallenges and (userChallenges.indexOf(chal.id) != -1) - _.each ['habit', 'daily', 'todo', 'reward'], (type) -> - _.each chal["#{type}s"], (task) -> - task.tags = tags - task.challenge = chal.id - task.group = {id: chal.group.id, type: chal.group.type} - model.push("_#{type}List", task) + ### + Sync any updates to challenges since last refresh. Do it after cron, so we don't punish them for new tasks + This is challenge->user sync. user->challenge happens when user interacts with their tasks + ### + app.on 'ready', (model) -> + window.setTimeout -> + _.each model.get('groups'), (g) -> + if (@uid in g.members) and g.challenges + _.each(g.challenges, ->app.challenges.syncChalToUser g) true + , 500 - appExports.challengeUnsubscribe = (e) -> - chal = e.get() - i = user.get('challenges')?.indexOf chal.id - user.remove("challenges.#{i}") if i? and i != -1 - _.each ['habit', 'daily', 'todo', 'reward'], (type) -> - _.each chal["#{type}s"], (task) -> - model.remove "_#{type}List", _.findIndex(model.get("_#{type}List",{id:task.id})) - model.del "_user.tasks.#{task.id}" - true + ### + Sync user to challenge (when they score, add to statistics) + ### + app.model.on "change", "_page.user.priv.tasks.*.value", (id, value, previous, passed) -> + ### Sync to challenge, but do it later ### + async.nextTick => + model = app.model + ctx = {model: model} + task = model.at "_page.user.priv.tasks.#{id}" + tobj = task.get() + pub = model.get "_page.user.pub" + + if (chalTask = helpers.taskInChallenge.call ctx, tobj)? and chalTask.get() + chalTask.increment "value", value - previous + chal = model.at "groups.#{tobj.group.id}.challenges.#{tobj.challenge}" + chalUser = -> helpers.indexedAt.call(ctx, chal.path(), 'members', {id:pub.id}) + cu = chalUser() + unless cu?.get() + chal.push "members", {id: pub.id, name: model.get(pub.profile.name)} + cu = model.at chalUser() + else + cu.set 'name', pub.profile.name # update their name incase it changed + cu.set "#{tobj.type}s.#{tobj.id}", + value: tobj.value + history: tobj.history + + ### + Render graphs for user scores when the "Challenges" tab is clicked + ### + + ### + TODO + 1) on main tab click or party + * sort & render graphs for party + 2) guild -> all guilds + 3) public -> all public + ### + + + ### + $('#profile-challenges-tab-link').on 'shown', -> + async.each _.toArray(model.get('groups')), (g) -> + async.each _.toArray(g.challenges), (chal) -> + async.each _.toArray(chal.tasks), (task) -> + async.each _.toArray(chal.members), (member) -> + if (history = member?["#{task.type}s"]?[task.id]?.history) and !!history + data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) + options = + backgroundColor: { fill:'transparent' } + width: 150 + height: 50 + chartArea: width: '80%', height: '80%' + axisTitlePosition: 'none' + legend: position: 'bottom' + hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines... + vAxis: gridlines: color: 'transparent' + chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] + chart.draw(data, options) + ### + + app.fn + challenges: + + ### + Create + ### + create: (e,el) -> + [type, gid] = [$(el).attr('data-type'), $(el).attr('data-gid')] + cid = @model.id() + @model.set '_page.new.challenge', + id: cid + name: '' + habits: [] + dailys: [] + todos: [] + rewards: [] + user: + uid: @uid + name: @pub.get('profile.name') + group: {type, id:gid} + timestamp: +new Date + + ### + Save + ### + save: -> + newChal = @model.get('_page.new.challenge') + [gid, cid] = [newChal.group.id, newChal.id] + @model.push "_page.lists.challenges.#{gid}", newChal, -> + app.browser.growlNotification('Challenge Created','success') + app.challenges.discard() + app.browser.resetDom() # something is going absolutely haywire here, all model data at end of reflist after chal created + + ### + Toggle Edit + ### + toggleEdit: (e, el) -> + path = "_page.editing.challenges.#{$(el).attr('data-id')}" + @model.set path, !@model.get(path) + + ### + Discard + ### + discard: -> + @model.del '_page.new.challenge' + + ### + Delete + ### + delete: (e) -> + return unless confirm("Delete challenge, are you sure?") is true + e.at().remove() + + ### + Add challenge name as a tag for user + ### + syncChalToUser: (chal) -> + return unless chal + ### Sync tags ### + tags = @priv.get('tags') or [] + idx = _.findIndex tags, {id: chal.id} + if ~idx and (tags[idx].name isnt chal.name) + ### update the name - it's been changed since ### + @priv.set "tags.#{idx}.name", chal.name + else + @priv.push 'tags', {id: chal.id, name: chal.name, challenge: true} + + tags = {}; tags[chal.id] = true + _.each chal.habits.concat(chal.dailys.concat(chal.todos.concat(chal.rewards))), (task) => + _.defaults task, { tags, challenge: chal.id, group: {id: chal.group.id, type: chal.group.type} } + path = "tasks.#{task.id}" + if @priv.get path + @priv.set path, _.defaults(@priv.get(path), task) + else + @model.push "_page.lists.tasks.#{@uid}.#{task.type}s", task + true + + ### + Subscribe + ### + subscribe: (e) -> + chal = e.get() + ### Add all challenge's tasks to user's tasks ### + currChallenges = @pub.get('challenges') + @pub.unshift('challenges', chal.id) unless currChallenges and ~currChallenges.indexOf(chal.id) + e.at().push "members", + id: @uid + name: @pub.get('profile.name') + app.challenges.syncChalToUser(chal) + + ### + -------------------------- + Unsubscribe functions + -------------------------- + ### + + unsubscribe: (chal, keep=true) -> + + ### Remove challenge from user ### + i = @pub.get('challenges')?.indexOf(chal.id) + if i? and ~i + @pub.remove("challenges", i, 1) + + ### Remove user from challenge ### + if ~(i = _.findIndex chal.members, {id: @uid}) + @model.remove "groups.#{chal.group.id}.challenges.#{chal.id}.members", i, 1 + + ### Remove tasks from user ### + async.each chal.habits.concat(chal.dailys.concat(chal.todos.concat(chal.rewards))), (task) => + if keep is true + @priv.del "tasks.#{task.id}.challenge" + else + path = "_page.lists.tasks.#{@uid}.#{task.type}s" + if ~(i = _.findIndex(@model.get(path), {id:task.id})) + @model.remove(path, i, 1) + true + + taskUnsubscribe: (e, el) -> + + ### + since the challenge was deleted, we don't have its data to unsubscribe from - but we have the vestiges on the task + FIXME this is a really dumb way of doing this + ### + tasks = @priv.get('tasks') + tobj = tasks[$(el).attr("data-tid")] + deletedChal = + id: tobj.challenge + members: [@uid] + habits: _.where tasks, {type: 'habit', challenge: tobj.challenge} + dailys: _.where tasks, {type: 'daily', challenge: tobj.challenge} + todos: _.where tasks, {type: 'todo', challenge: tobj.challenge} + rewards: _.where tasks, {type: 'reward', challenge: tobj.challenge} + + switch $(el).attr('data-action') + when 'keep' + @priv.del "tasks.#{tobj.id}.challenge" + @priv.del "tasks.#{tobj.id}.group" + when 'keep-all' + app.challenges.unsubscribe.call @, deletedChal, true + when 'remove' + path = "_page.lists.tasks.#{@uid}.#{tobj.type}s" + if ~(i = _.findIndex @model.get(path), {id: tobj.id}) + @model.remove path, i + when 'remove-all' + app.challenges.unsubscribe.call @, deletedChal, false + + challengeUnsubscribe: (e, el) -> + $(el).popover('destroy').popover({ + html: true + placement: 'top' + trigger: 'manual' + title: 'Unsubscribe From Challenge And:' + content: """ + Remove Tasks
+ Keep Tasks
+ Cancel
+ """ + }).popover('show') + $('.challenge-unsubscribe-and-remove').click => app.challenges.unsubscribe.call @, e.get(), false + $('.challenge-unsubscribe-and-keep').click => app.challenges.unsubscribe.call @, e.get(), true + $('[class^=challenge-unsubscribe]').click => $(el).popover('destroy') \ No newline at end of file diff --git a/views/options/challenges.html b/views/options/challenges.html index c5ef9cba1e..edff5ba0e0 100644 --- a/views/options/challenges.html +++ b/views/options/challenges.html @@ -1,72 +1,58 @@ -
- + + <@headers> + + + + -
+ + {{#unless _page.party.id}} + Join a party first. + {{else}} + + {{/}} + -
- {{#unless _party.id}} - Join a party first. - {{else}} - {#if _challenge.new} - - {else} - - - {#each _party.challenges as :challenge} - - {/} - {/} + + +
+ {{#each _page.guilds as :guild}} +
+ +
{{/}}
+
-
- -
- {{#each _guilds as :guild}} -
- {#if _challenge.new} - - {else} - - {#each :guild.challenges as :challenge} - - {/} -
- {/} -
- {{/}} -
-
+ + + -
- {#if _challenge.new} - - {else} - - {#each _habitRPG.challenges as :challenge} - - {/} - {/} -
+ -
+ +
+ +
+
+ + {#each @list as :challenge} + + {/} +
- +
- {@challenge.name} (by {@challenge.user}) + {@challenge.name} (by {@challenge.user.name})
- - - - {#if and(not(_editing.challenges[@challenge.id]),equal(@challenge.uid,_user.id))} + {#if and(not(_page.editing.challenges[@challenge.id]),equal(@challenge.user.uid,_session.userId))} {else} {/} - {#if _editing.challenges[@challenge.id]} + {#if _page.editing.challenges[@challenge.id]}
- +
{{#with @challenge}} - Delete + Delete {{/}} {/} {#if @challenge.description}
{@challenge.description}
{/}
+ rewards={@challenge.rewards} + />

Statistics

- {#each @challenge.users as :member} + {#each @challenge.members as :member}

{:member.name}

- +
- +
- +
{/} @@ -146,11 +132,11 @@
{@header}
- {#each @challenge[@taskType]s as :task} + {#each @list as :task}
- {:task.text}: {challengeMemberScore(@member,@taskType,:task.id)} + {:task.text}: {challengeMemberScore(@member,:task)}
@@ -160,67 +146,25 @@ - Create {{@text}} Challenge + Create {{@text}} Challenge -
+
- +
- +
- - -
-
- - + \ No newline at end of file diff --git a/views/options/groups/group.jade b/views/options/groups/group.jade index 505e50bdb7..60e495dcf3 100644 --- a/views/options/groups/group.jade +++ b/views/options/groups/group.jade @@ -78,24 +78,30 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' input.btn(type='submit', value='Invite') - //-accordion-group(heading='Challenges') - span.label - i.icon-bullhorn - | Challenges - | coming soon! - a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details - //-{#if group.challenges} - //- - //- {#each group.challenges as :challenge} - //- - //- {/} - //-
- //- {:challenge.name} - //-
- //- Visit the Challenges for more information. - //-{else} - //- No challenges yet, visit the Challenges tab to create one. - //-{/} + //.accordion-group + .accordion-heading + a.accordion-toggle(data-toggle='collapse', data-parent='#accordion-{{@group.id}}-parent', href='#accordion-{{@group.id}}-challenges') Challenges + #accordion-{{@group.id}}-challenges.accordion-body.collapse + .accordion-inner + span.label + i.icon-bullhorn + | Challenges + | coming soon! + a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details + // + // {#if @group.challenges} + // + // {#each @group.challenges as :challenge} + // + // {/} + //
+ // {:challenge.name} + //
+ // Visit the Challenges for more information. + // {else} + // No challenges yet, visit the Challenges tab to create one. + // {/} + a.btn.btn-danger(data-id='{{group.id}}', ng-click='leave(group)') Leave .span8 diff --git a/views/tasks/task.jade b/views/tasks/task.jade index a2a2abdcde..c0a47fdb9b 100644 --- a/views/tasks/task.jade +++ b/views/tasks/task.jade @@ -18,13 +18,16 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel') i.icon-remove(ng-hide='!task._editing') //- challenges - // {{#if task.challenge}} - // {{#if brokenChallengeLink(task)}} - // - // {{else}} - // - // {{/}} + // {{#if :task.challenge}} + // {{#if brokenChallengeLink(:task)}} + // + // {{else}} + // + // {{/}} // {{else}} + // + // + // {{/}} // delete a(ng-click='remove(task)', tooltip='Delete') @@ -78,12 +81,24 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use // edit/options dialog .task-options(ng-show='task._editing') - // {{#if brokenChallengeLink(task)}} - //
- //

Broken Challenge Link: this task was part of a challenge, but (a) challenge (or containing group) has been deleted, or (b) the task was deleted from the challenge.

- //

Keep | Keep all from challenge | Delete | Delete all from challenge

- //
- // {{/}} + // + // {#if brokenChallengeLink(:task)} + //
+ // {{#if groups[:task.group.id].challenges[:task.challenge]}} + //

Broken Challenge Link: this task was part of a challenge, but has been removed from it. What would you like to do?

+ //

+ // Keep It |  + // Remove It + //

+ // {{else}} + //

Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What would you like to do with these poor lil' orphans?

+ //

+ // Keep Them |  + // Remove Them + //

+ // {{/}} + //
+ // {/} form(ng-controller="TaskDetailsCtrl", ng-submit='save(task)') // text & notes fieldset.option-group From e45d8307e713bb2b7732beb59567f4fa2cd102f2 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 26 Oct 2013 17:23:52 -0700 Subject: [PATCH 02/15] Subdocs & script templates. Migrate the API from User.tasks => User.habits/dailys/todos/rewards. Move /#/tasks & /#/options page loading from server-sent html to everything loaded in the page as script templates (including necessary fixes for adsense). NOTE: this commit won't work, it depends a bit on the *next* commit with Challenges functionality, but I wanted to separate it out a bit for clarity --- public/js/app.js | 6 +- public/js/controllers/filtersCtrl.js | 2 +- public/js/controllers/rootCtrl.js | 5 + public/js/controllers/taskDetailsCtrl.js | 53 --- public/js/controllers/tasksCtrl.js | 118 ++--- public/js/directives/directives.js | 50 ++ public/js/services/userServices.js | 1 - src/controllers/user.js | 427 +++++------------- src/models/task.js | 41 ++ src/models/user.js | 57 +-- src/routes/pages.js | 8 - views/index.jade | 5 +- views/{tasks => main}/filters.jade | 0 views/main/index.jade | 4 + views/options/index.jade | 106 ++--- .../index.jade => shared/tasks/lists.jade} | 32 +- views/{ => shared}/tasks/task.jade | 50 +- views/tasks/ads.jade | 25 - 18 files changed, 415 insertions(+), 575 deletions(-) delete mode 100644 public/js/controllers/taskDetailsCtrl.js create mode 100644 src/models/task.js rename views/{tasks => main}/filters.jade (100%) create mode 100644 views/main/index.jade rename views/{tasks/index.jade => shared/tasks/lists.jade} (62%) rename views/{ => shared}/tasks/task.jade (80%) delete mode 100644 views/tasks/ads.jade diff --git a/public/js/app.js b/public/js/app.js index 3ae6e6e098..73936bae9a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,7 +1,7 @@ "use strict"; window.habitrpg = angular.module('habitrpg', - ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'sharedServices', 'authServices', 'notificationServices', 'guideServices', 'ui.bootstrap', 'ui.keypress']) + ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices', 'sharedServices', 'authServices', 'notificationServices', 'guideServices', 'ui.bootstrap', 'ui.keypress']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') @@ -12,8 +12,8 @@ window.habitrpg = angular.module('habitrpg', function($routeProvider, $httpProvider, STORAGE_SETTINGS_ID) { $routeProvider //.when('/login', {templateUrl: 'views/login.html'}) - .when('/tasks', {templateUrl: 'partials/tasks'}) - .when('/options', {templateUrl: 'partials/options'}) + .when('/tasks', {templateUrl: 'templates/habitrpg-main.html'}) + .when('/options', {templateUrl: 'templates/habitrpg-options.html'}) .otherwise({redirectTo: '/tasks'}); diff --git a/public/js/controllers/filtersCtrl.js b/public/js/controllers/filtersCtrl.js index 86444d219c..3839074a6c 100644 --- a/public/js/controllers/filtersCtrl.js +++ b/public/js/controllers/filtersCtrl.js @@ -34,7 +34,7 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'API_URL', ' delete user.filters[tag.id]; user.tags.splice($index,1); // remove tag from all tasks - _.each(user.tasks, function(task) { + _.each(user.habits.concat(user.dailys).concat(user.todos).concat(user.rewards), function(task) { delete task.tags[tag.id]; }); User.log({op:'delTag',data:{'tag':tag.id}}) diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 0726686180..1ad39e2285 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -12,6 +12,11 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $rootScope.settings = User.settings; $rootScope.flash = {errors: [], warnings: []}; + // indexOf helper + $scope.indexOf = function(haystack, needle){ + return ~haystack.indexOf(needle); + } + $scope.safeApply = function(fn) { var phase = this.$root.$$phase; if(phase == '$apply' || phase == '$digest') { diff --git a/public/js/controllers/taskDetailsCtrl.js b/public/js/controllers/taskDetailsCtrl.js deleted file mode 100644 index 1d67b72277..0000000000 --- a/public/js/controllers/taskDetailsCtrl.js +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -habitrpg.controller("TaskDetailsCtrl", ['$scope', '$rootScope', '$location', 'User', - function($scope, $rootScope, $location, User) { - $scope.save = function(task) { - var log, setVal; - setVal = function(k, v) { - var op; - if (typeof v !== "undefined") { - op = { - op: "set", - data: {} - }; - op.data["tasks." + task.id + "." + k] = v; - return log.push(op); - } - }; - log = []; - setVal("text", task.text); - setVal("notes", task.notes); - setVal("priority", task.priority); - setVal("tags", task.tags); - if (task.type === "habit") { - setVal("up", task.up); - setVal("down", task.down); - } else if (task.type === "daily") { - setVal("repeat", task.repeat); - // TODO we'll remove this once rewrite's running for a while. This was a patch for derby issues - setVal("streak", task.streak); - - } else if (task.type === "todo") { - setVal("date", task.date); - } else { - if (task.type === "reward") { - setVal("value", task.value); - } - } - User.log(log); - task._editing = false; - }; - $scope.cancel = function() { - /* reset $scope.task to $scope.originalTask - */ - - var key; - for (key in $scope.task) { - $scope.task[key] = $scope.originalTask[key]; - } - $scope.originalTask = null; - $scope.editedTask = null; - $scope.editing = false; - }; -}]); diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index 0c48e2758e..a33919ce0c 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -2,35 +2,6 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', 'Algos', 'Helpers', 'Notification', function($scope, $rootScope, $location, User, Algos, Helpers, Notification) { - /*FIXME - */ - $scope.taskLists = [ - { - header: 'Habits', - type: 'habit', - placeHolder: 'New Habit', - main: true, - editable: true - }, { - header: 'Dailies', - type: 'daily', - placeHolder: 'New Daily', - main: true, - editable: true - }, { - header: 'Todos', - type: 'todo', - placeHolder: 'New Todo', - main: true, - editable: true - }, { - header: 'Rewards', - type: 'reward', - placeHolder: 'New Reward', - main: true, - editable: true - } - ]; $scope.score = function(task, direction) { if (task.type === "reward" && User.user.stats.gp < task.value){ return Notification.text('Not enough GP.'); @@ -42,18 +13,20 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' $scope.addTask = function(list) { var task = window.habitrpgShared.helpers.taskDefaults({text: list.newTask, type: list.type}, User.user.filters); - User.user[list.type + "s"].unshift(task); - // $scope.showedTasks.unshift newTask # FIXME what's thiss? + list.tasks.unshift(task); User.log({op: "addTask", data: task}); delete list.newTask; }; - /*Add the new task to the actions log - */ + /** + * Add the new task to the actions log + */ $scope.clearDoneTodos = function() {}; + + /** + * This is calculated post-change, so task.completed=true if they just checked it + */ $scope.changeCheck = function(task) { - /* This is calculated post-change, so task.completed=true if they just checked it - */ if (task.completed) { $scope.score(task, "up"); } else { @@ -66,27 +39,59 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' // uhoh! our first name conflict with habitrpg-shared/helpers, we gotta resovle that soon. $rootScope.clickRevive = function() { window.habitrpgShared.algos.revive(User.user); - User.log({ - op: "revive" - }); + User.log({ op: "revive" }); }; - $scope.toggleEdit = function(task){ - task._editing = !task._editing; - if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; + $scope.removeTask = function(list, $index) { + if (!confirm("Are you sure you want to delete this task?")) return; + User.log({ op: "delTask", data: list[$index] }); + list.splice($index, 1); }; - $scope.remove = function(task) { - var tasks; - if (confirm("Are you sure you want to delete this task?") !== true) { - return; + $scope.saveTask = function(task) { + var setVal = function(k, v) { + var op; + if (typeof v !== "undefined") { + op = { op: "set", data: {} }; + op.data["tasks." + task.id + "." + k] = v; + return log.push(op); + } + }; + var log = []; + setVal("text", task.text); + setVal("notes", task.notes); + setVal("priority", task.priority); + setVal("tags", task.tags); + if (task.type === "habit") { + setVal("up", task.up); + setVal("down", task.down); + } else if (task.type === "daily") { + setVal("repeat", task.repeat); + // TODO we'll remove this once rewrite's running for a while. This was a patch for derby issues + setVal("streak", task.streak); + + } else if (task.type === "todo") { + setVal("date", task.date); + } else { + if (task.type === "reward") { + setVal("value", task.value); + } } - tasks = User.user[task.type + "s"]; - User.log({ - op: "delTask", - data: task - }); - tasks.splice(tasks.indexOf(task), 1); + User.log(log); + task._editing = false; + }; + + /** + * Reset $scope.task to $scope.originalTask + */ + $scope.cancel = function() { + var key; + for (key in $scope.task) { + $scope.task[key] = $scope.originalTask[key]; + } + $scope.originalTask = null; + $scope.editedTask = null; + $scope.editing = false; }; /* @@ -123,4 +128,15 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' User.log({op: 'clear-completed'}); } + /** + * See conversation on http://productforums.google.com/forum/#!topic/adsense/WYkC_VzKwbA, + * Adsense is very sensitive. It must be called once-and-only-once for every , else things break. + * Additionally, angular won't run javascript embedded into a script template, so we can't copy/paste + * the html provided by adsense - we need to run this function post-link + */ + $scope.initAds = function(){ + $.getScript('//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js'); + (window.adsbygoogle = window.adsbygoogle || []).push({}); + } + }]); diff --git a/public/js/directives/directives.js b/public/js/directives/directives.js index 3a1f2f302d..94ebc33417 100644 --- a/public/js/directives/directives.js +++ b/public/js/directives/directives.js @@ -109,3 +109,53 @@ habitrpg.directive('habitrpgSortable', ['User', function(User) { }; }); })() + +habitrpg + .directive('habitrpgTasks', ['$rootScope', 'User', function($rootScope, User) { + return { + restrict: 'EA', + templateUrl: 'templates/habitrpg-tasks.html', + //transclude: true, + //scope: { + // main: '@', // true if it's the user's main list + // obj: '=' + //}, + controller: function($scope, $rootScope){ + $scope.editTask = function(task){ + task._editing = !task._editing; + if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; + }; + }, + link: function(scope, element, attrs) { + scope.obj = scope[attrs.obj]; + scope.main = attrs.main; + + + scope.lists = [ + { + header: 'Habits', + type: 'habit', + placeHolder: 'New Habit', + tasks: scope.obj.habits + }, { + header: 'Dailies', + type: 'daily', + placeHolder: 'New Daily', + tasks: scope.obj.dailys + }, { + header: 'Todos', + type: 'todo', + placeHolder: 'New Todo', + tasks: scope.obj.todos + }, { + header: 'Rewards', + type: 'reward', + placeHolder: 'New Reward', + tasks: scope.obj.rewards + } + ]; + scope.editable = true; + } + } + }]); + diff --git a/public/js/services/userServices.js b/public/js/services/userServices.js index e25cc89a5c..11131a4bed 100644 --- a/public/js/services/userServices.js +++ b/public/js/services/userServices.js @@ -58,7 +58,6 @@ angular.module('userServices', []). $http.post(API_URL + '/api/v1/user/batch-update', sent, {params: {data:+new Date, _v:user._v}}) .success(function (data, status, heacreatingders, config) { - data.tasks = _.toArray(data.tasks); //make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data. if (!queue.length) { //we can't do user=data as it will not update user references in all other angular controllers. diff --git a/src/controllers/user.js b/src/controllers/user.js index 7a3186d73d..54b92b20c3 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -55,98 +55,28 @@ api.marketBuy = function(req, res, next){ --------------- */ - -/* -// FIXME put this in helpers, so mobile & web can us it too -// FIXME actually, move to mongoose -*/ - - -function taskSanitizeAndDefaults(task) { - var _ref; - if (task.id == null) { - task.id = helpers.uuid(); - } - task.value = ~~task.value; - if (task.type == null) { - task.type = 'habit'; - } - if (_.isString(task.text)) { - task.text = sanitize(task.text).xss(); - } - if (_.isString(task.text)) { - task.notes = sanitize(task.notes).xss(); - } - if (task.type === 'habit') { - if (!_.isBoolean(task.up)) { - task.up = true; - } - if (!_.isBoolean(task.down)) { - task.down = true; - } - } - if ((_ref = task.type) === 'daily' || _ref === 'todo') { - if (!_.isBoolean(task.completed)) { - task.completed = false; - } - } - if (task.type === 'daily') { - if (task.repeat == null) { - task.repeat = { - m: true, - t: true, - w: true, - th: true, - f: true, - s: true, - su: true - }; - } - } - return task; -}; - /* Validate task */ - - api.verifyTaskExists = function(req, res, next) { - /* If we're updating, get the task from the user*/ - - var task; - task = res.locals.user.tasks[req.params.id]; - if (_.isEmpty(task)) { - return res.json(400, { - err: "No task found." - }); - } + // If we're updating, get the task from the user + var task = res.locals.user.tasks[req.params.id]; + if (_.isEmpty(task)) return res.json(400, {err: "No task found."}); res.locals.task = task; return next(); }; -function addTask(user, task) { - taskSanitizeAndDefaults(task); - user.tasks[task.id] = task; - user["" + task.type + "Ids"].unshift(task.id); - return task; -}; - -/* Override current user.task with incoming values, then sanitize all values*/ - - -function updateTask(user, id, incomingTask) { - return user.tasks[id] = taskSanitizeAndDefaults(_.defaults(incomingTask, user.tasks[id])); -}; - function deleteTask(user, task) { - var i, ids; - delete user.tasks[task.id]; - if ((ids = user["" + task.type + "Ids"]) && ~(i = ids.indexOf(task.id))) { - return ids.splice(i, 1); - } + user[task.type+'s'].id(task.id).remove(); }; +function addTask(user, task) { + var type = task.type || 'habit' + user[type+'s'].unshift(task); + // FIXME will likely have to use taskSchema instead, so we can populate the defaults, add the _id, and return the added task + return user[task.type+'s'][0]; +} + /* API Routes --------------- @@ -158,52 +88,42 @@ function deleteTask(user, task) { Export it also so we can call it from deprecated.coffee */ api.scoreTask = function(req, res, next) { - - // FIXME this is all uglified from coffeescript compile, clean this up - - var delta, direction, existing, id, task, user, _ref, _ref1, _ref2, _ref3, _ref4; - _ref = req.params, id = _ref.id, direction = _ref.direction; + var id = req.params.id, + direction = req.params.direction, + user = res.locals.user, + task; // Send error responses for improper API call - if (!id) { - return res.json(500, { - err: ':id required' - }); - } + if (!id) return res.json(500, {err: ':id required'}); if (direction !== 'up' && direction !== 'down') { - return res.json(500, { - err: ":direction must be 'up' or 'down'" - }); + return res.json(500, {err: ":direction must be 'up' or 'down'"}); } - user = res.locals.user; - /* If exists already, score it*/ - - if ((existing = user.tasks[id])) { - /* Set completed if type is daily or todo and task exists*/ - - if ((_ref1 = existing.type) === 'daily' || _ref1 === 'todo') { + // If exists already, score it + var existing; + if (existing = user.tasks[id]) { + // Set completed if type is daily or todo and task exists + if (existing.type === 'daily' || existing.type === 'todo') { existing.completed = direction === 'up'; } } else { - /* If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it*/ - + // If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it task = { id: id, value: 0, - type: ((_ref2 = req.body) != null ? _ref2.type : void 0) || 'habit', - text: ((_ref3 = req.body) != null ? _ref3.title : void 0) || id, + type: req.body.type || 'habit', + text: req.body.title || id, notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task." }; if (task.type === 'habit') { task.up = task.down = true; } - if ((_ref4 = task.type) === 'daily' || _ref4 === 'todo') { + if (task.type === 'daily' || task.type === 'todo') { task.completed = direction === 'up'; } addTask(user, task); } task = user.tasks[id]; - delta = algos.score(user, task, direction); + var delta = algos.score(user, task, direction); //user.markModified('flags'); user.save(function(err, saved) { if (err) return res.json(500, {err: err}); @@ -213,42 +133,29 @@ api.scoreTask = function(req, res, next) { }); }; -/* - Get all tasks -*/ - - +/** + * Get all tasks + */ api.getTasks = function(req, res, next) { - var tasks, types, _ref; - types = (_ref = req.query.type) === 'habit' || _ref === 'todo' || _ref === 'daily' || _ref === 'reward' ? [req.query.type] : ['habit', 'todo', 'daily', 'reward']; - tasks = _.toArray(_.filter(res.locals.user.tasks, function(t) { - var _ref1; - return _ref1 = t.type, __indexOf.call(types, _ref1) >= 0; - })); - return res.json(200, tasks); + if (req.query.type) { + return res.json(user[req.query.type+'s']); + } else { + return res.json(_.toArray(user.tasks)); + } }; -/* - Get Task -*/ - - +/** + * Get Task + */ api.getTask = function(req, res, next) { - var task; - task = res.locals.user.tasks[req.params.id]; - if (_.isEmpty(task)) { - return res.json(400, { - err: "No task found." - }); - } + var task = res.locals.user.tasks[req.params.id]; + if (_.isEmpty(task)) return res.json(400, {err: "No task found."}); return res.json(200, task); }; -/* - Delete Task -*/ - - +/** + * Delete Task + */ api.deleteTask = function(req, res, next) { deleteTask(res.locals.user, res.locals.task); res.locals.user.save(function(err) { @@ -263,113 +170,69 @@ api.deleteTask = function(req, res, next) { api.updateTask = function(req, res, next) { - var id, user; - user = res.locals.user; - id = req.params.id; - updateTask(user, id, req.body); - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } - return res.json(200, _.findWhere(saved.toJSON().tasks, { - id: id - })); + var user = res.locals.user; + var task = user.tasks[req.params.id]; + user[task.type+'s'][_.findIndex(user[task.type+'s'],{id:task.id})] = req.body; + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}) + return res.json(200, saved.tasks[id]); }); }; -/* - Update tasks (plural). This will update, add new, delete, etc all at once. - Should we keep this? -*/ - - +/** + * Update tasks (plural). This will update, add new, delete, etc all at once. + * TODO Should we keep this? + */ api.updateTasks = function(req, res, next) { - var tasks, user; - user = res.locals.user; - tasks = req.body; + var user = res.locals.user; + var tasks = req.body; _.each(tasks, function(task, idx) { if (task.id) { - /*delete*/ - + // delete if (task.del) { deleteTask(user, task); - task = { - deleted: true - }; + task = {deleted: true}; } else { - /* Update*/ - - updateTask(user, task.id, task); + // Update + // updateTask(user, task.id, task); //FIXME } } else { - /* Create*/ - + // Create task = addTask(user, task); } - return tasks[idx] = task; + tasks[idx] = task; }); - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); return res.json(201, tasks); }); }; api.createTask = function(req, res, next) { - var task, user; - user = res.locals.user; - task = addTask(user, req.body); - return user.save(function(err) { - if (err) { - return res.json(500, { - err: err - }); - } + var user = res.locals.user; + var task = addTask(user, req.body); + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); return res.json(201, task); }); }; api.sortTask = function(req, res, next) { - var from, id, path, to, type, user, _ref; - id = req.params.id; - _ref = req.body, to = _ref.to, from = _ref.from, type = _ref.type; - user = res.locals.user; - path = "" + type + "Ids"; - user[path].splice(to, 0, user[path].splice(from, 1)[0]); - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } - return res.json(200, saved.toJSON()[path]); + var id = req.params.id; + var to = req.body.to, from = req.body.from, type = req.body.type; + var user = res.locals.user; + user[type+'s'].splice(to, 0, user[type+'s'].splice(from, 1)[0]); + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); + return res.json(200, saved.toJSON()[type+'s']); }); }; api.clearCompleted = function(req, res, next) { - var completedIds, todoIds, user; - user = res.locals.user; - completedIds = _.pluck(_.where(user.tasks, { - type: 'todo', - completed: true - }), 'id'); - todoIds = user.todoIds; - _.each(completedIds, function(id) { - delete user.tasks[id]; - return true; - }); - user.todoIds = _.difference(todoIds, completedIds); + var user = res.locals.user; + user.todos = _.where(user.todos, {completed: false}); return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } + if (err) return res.json(500, {err: err}); return res.json(saved); }); }; @@ -379,31 +242,21 @@ api.clearCompleted = function(req, res, next) { Items ------------------------------------------------------------------------ */ - - api.buy = function(req, res, next) { var hasEnough, type, user; user = res.locals.user; type = req.params.type; if (type !== 'weapon' && type !== 'armor' && type !== 'head' && type !== 'shield' && type !== 'potion') { - return res.json(400, { - err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'" - }); + return res.json(400, {err: ":type must be in one of: 'weapon', 'armor', 'head', 'shield', 'potion'"}); } hasEnough = items.buyItem(user, type); if (hasEnough) { return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } + if (err) return res.json(500, {err: err}); return res.json(200, saved.toJSON().items); }); } else { - return res.json(200, { - err: "Not enough GP" - }); + return res.json(200, {err: "Not enough GP"}); } }; @@ -413,11 +266,9 @@ api.buy = function(req, res, next) { ------------------------------------------------------------------------ */ -/* - Get User -*/ - - +/** + * Get User + */ api.getUser = function(req, res, next) { var user = res.locals.user.toJSON(); user.stats.toNextLevel = algos.tnl(user.stats.lvl); @@ -430,12 +281,10 @@ api.getUser = function(req, res, next) { return res.json(200, user); }; -/* - Update user - FIXME add documentation here +/** + * Update user + * FIXME add documentation here */ - - api.updateUser = function(req, res, next) { var acceptableAttrs, errors, user; user = res.locals.user; @@ -483,8 +332,7 @@ api.updateUser = function(req, res, next) { }; api.cron = function(req, res, next) { - var user; - user = res.locals.user; + var user = res.locals.user; algos.cron(user); if (user.isModified()) { res.locals.wasModified = true; @@ -494,52 +342,36 @@ api.cron = function(req, res, next) { }; api.revive = function(req, res, next) { - var user; - user = res.locals.user; + var user = res.locals.user; algos.revive(user); - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); return res.json(200, saved); }); }; api.reroll = function(req, res, next) { - var user; - user = res.locals.user; - if (user.balance < 1) { - return res.json(401, { - err: "Not enough tokens." - }); - } + var user = res.locals.user; + if (user.balance < 1) return res.json(401, {err: "Not enough tokens."}); user.balance -= 1; - _.each(user.tasks, function(task) { - if (task.type !== 'reward') { - user.tasks[task.id].value = 0; - } - return true; - }); + _.each(['habits','dailys','todos'], function(type){ + _.each([user[type+'s']], function(task){ + task.value = 0; + }) + }) user.stats.hp = 50; - return user.save(function(err, saved) { - if (err) { - return res.json(500, { - err: err - }); - } + user.save(function(err, saved) { + if (err) return res.json(500, {err: err}); return res.json(200, saved); }); }; api.reset = function(req, res){ var user = res.locals.user; - user.tasks = {}; - - _.each(['habit', 'daily', 'todo', 'reward'], function(type) { - user[type + "Ids"] = []; - }); + user.habits = []; + user.dailys = []; + user.todos = []; + user.rewards = []; user.stats.hp = 50; user.stats.lvl = 1; @@ -675,9 +507,11 @@ api.deleteTag = function(req, res){ delete user.filters[tag.id]; user.tags.splice(i,1); // remove tag from all tasks - _.each(user.tasks, function(task) { - delete user.tasks[task.id].tags[tag.id]; - }); + _.each(['habits','dailys','todos','rewards'], function(type){ + _.each(user[type], function(task){ + delete task.tags[tag.id]; + }) + }) user.save(function(err,saved){ if (err) return res.json(500, {err: err}); // Need to use this until we found a way to update the ui for tasks when a tag is deleted @@ -695,22 +529,16 @@ api.deleteTag = function(req, res){ Run a bunch of updates all at once ------------------------------------------------------------------------ */ - - api.batchUpdate = function(req, res, next) { - var actions, oldJson, oldSend, performAction, user, _ref; - user = res.locals.user; - oldSend = res.send; - oldJson = res.json; - performAction = function(action, cb) { - /* - # TODO come up with a more consistent approach here. like: - # req.body=action.data; delete action.data; _.defaults(req.params, action) - # Would require changing action.dir on mobile app - */ + var user = res.locals.user; + var oldSend = res.send; + var oldJson = res.json; + var performAction = function(action, cb) { - var _ref; - req.params.id = (_ref = action.data) != null ? _ref.id : void 0; + // TODO come up with a more consistent approach here. like: + // req.body=action.data; delete action.data; _.defaults(req.params, action) + // Would require changing action.dir on mobile app + req.params.id = action.data && action.data.id; req.params.direction = action.dir; req.params.type = action.type; req.body = action.data; @@ -764,27 +592,22 @@ api.batchUpdate = function(req, res, next) { break; } }; - /* Setup the array of functions we're going to call in parallel with async*/ - actions = _.transform((_ref = req.body) != null ? _ref : [], function(result, action) { + // Setup the array of functions we're going to call in parallel with async + var actions = _.transform(req.body || [], function(result, action) { if (!_.isEmpty(action)) { - return result.push(function(cb) { - return performAction(action, cb); + result.push(function(cb) { + performAction(action, cb); }); } }); - /* call all the operations, then return the user object to the requester*/ - return async.series(actions, function(err) { - var response; + // call all the operations, then return the user object to the requester + async.series(actions, function(err) { res.json = oldJson; res.send = oldSend; - if (err) { - return res.json(500, { - err: err - }); - } - response = user.toJSON(); + if (err) return res.json(500, {err: err}); + var response = user.toJSON(); response.wasModified = res.locals.wasModified; if (response._tmp && response._tmp.drop) response.wasModified = true; @@ -794,7 +617,5 @@ api.batchUpdate = function(req, res, next) { }else{ res.json(200, {_v: response._v}); } - - return; }); }; \ No newline at end of file diff --git a/src/models/task.js b/src/models/task.js new file mode 100644 index 0000000000..8abdd9302e --- /dev/null +++ b/src/models/task.js @@ -0,0 +1,41 @@ +// User.js +// ======= +// Defines the user data model (schema) for use via the API. + +// Dependencies +// ------------ +var mongoose = require("mongoose"); +var Schema = mongoose.Schema; +var helpers = require('habitrpg-shared/script/helpers'); +var _ = require('lodash'); + +// Task Schema +// ----------- + +var TaskSchema = new Schema({ + history: [{date:Date, value:Number}], + _id:{type: String,'default': helpers.uuid}, + text: String, + notes: {type: String, 'default': ''}, + tags: Schema.Types.Mixed, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, + type: {type:String, 'default': 'habit'}, // habit, daily + up: {type: Boolean, 'default': true}, + down: {type: Boolean, 'default': true}, + value: {type: Number, 'default': 0}, + completed: {type: Boolean, 'default': false}, + priority: {type: String, 'default': '!'}, //'!!' // FIXME this should be a number or something + repeat: {type: Schema.Types.Mixed, 'default': {m:1, t:1, w:1, th:1, f:1, s:1, su:1} }, + streak: {type: Number, 'default': 0} +}); + +TaskSchema.methods.toJSON = function() { + var doc = this.toObject(); + doc.id = doc._id; + return doc; +} +TaskSchema.virtual('id').get(function(){ + return this._id; +}) + +module.exports.schema = TaskSchema; +module.exports.model = mongoose.model("Task", TaskSchema); \ No newline at end of file diff --git a/src/models/user.js b/src/models/user.js index 769d1f0450..74da652597 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -8,6 +8,7 @@ var mongoose = require("mongoose"); var Schema = mongoose.Schema; var helpers = require('habitrpg-shared/script/helpers'); var _ = require('lodash'); +var TaskSchema = require('./task').schema; // User Schema // ----------- @@ -204,26 +205,12 @@ var UserSchema = new Schema({ } ], - // ### Tasks Definition - // We can't define `tasks` until we move off Derby, since Derby requires dictionary of objects. When we're off, migrate - // to array of subdocs + challenges: [{type: 'String', ref:'Challenge'}], - tasks: Schema.Types.Mixed - /* - # history: {date, value} - # id - # notes - # tags { "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, - # text - # type - # up - # down - # value - # completed - # priority: '!!' - # repeat {m: true, t: true} - # streak - */ + habits: [TaskSchema], + dailys: [TaskSchema], + todos: [TaskSchema], + rewards: [TaskSchema], }, { strict: true, @@ -237,7 +224,8 @@ var UserSchema = new Schema({ // the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. // This will go away after the rewrite is complete -function transformTaskLists(doc) { +//FIXME use this in migration +/*function transformTaskLists(doc) { _.each(['habit', 'daily', 'todo', 'reward'], function(type) { // we use _.transform instead of a simple _.where in order to maintain sort-order doc[type + "s"] = _.reduce(doc[type + "Ids"], function(m, tid) { @@ -247,36 +235,37 @@ function transformTaskLists(doc) { return m; }, []); }); -} - -UserSchema.post('init', function(doc) { - transformTaskLists(doc); -}); +}*/ UserSchema.methods.toJSON = function() { var doc = this.toObject(); doc.id = doc._id; - transformTaskLists(doc); // we need to also transform for our server-side routes // FIXME? Is this a reference to `doc.filters` or just disabled code? Remove? - /* - // Remove some unecessary data as far as client consumers are concerned - //_.each(['habit', 'daily', 'todo', 'reward'], function(type) { - // delete doc["#{type}Ids"] - //}); - //delete doc.tasks - */ doc.filters = {}; doc._tmp = this._tmp; // be sure to send down drop notifs + // TODO why isnt' this happening automatically given the TaskSchema.methods.toJSON above? + _.each(['habits','dailys','todos','rewards'], function(type){ + _.each(doc[type],function(task){ + task.id = task._id; + }) + }) + return doc; }; +UserSchema.virtual('tasks').get(function () { + var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards); + var tasks = _.object(_.pluck(tasks,'id'), tasks); + return tasks; +}); + // FIXME - since we're using special @post('init') above, we need to flag when the original path was modified. // Custom setter/getter virtuals? UserSchema.pre('save', function(next) { - this.markModified('tasks'); + //this.markModified('tasks'); //FIXME //our own version incrementer this._v++; next(); diff --git a/src/routes/pages.js b/src/routes/pages.js index ab3e21cb5c..fe74e583d2 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -11,14 +11,6 @@ router.get('/', function(req, res) { }); }); -router.get('/partials/tasks', function(req, res) { - res.render('tasks/index', {env: res.locals.habitrpg}); -}); - -router.get('/partials/options', function(req, res) { - res.render('options', {env: res.locals.habitrpg}); -}); - // -------- Marketing -------- router.get('/splash.html', function(req, res) { diff --git a/views/index.jade b/views/index.jade index 1b6e26fab9..7e93871ae9 100644 --- a/views/index.jade +++ b/views/index.jade @@ -71,7 +71,6 @@ html script(type='text/javascript', src='/js/controllers/settingsCtrl.js') script(type='text/javascript', src='/js/controllers/statsCtrl.js') script(type='text/javascript', src='/js/controllers/tasksCtrl.js') - script(type='text/javascript', src='/js/controllers/taskDetailsCtrl.js') script(type='text/javascript', src='/js/controllers/filtersCtrl.js') script(type='text/javascript', src='/js/controllers/userCtrl.js') script(type='text/javascript', src='/js/controllers/groupsCtrl.js') @@ -91,6 +90,10 @@ html include ./shared/modals/index include ./shared/header/header + include ./shared/tasks/lists + include ./main/index + include ./options/index + #notification-area(ng-controller='NotificationCtrl') #wrap diff --git a/views/tasks/filters.jade b/views/main/filters.jade similarity index 100% rename from views/tasks/filters.jade rename to views/main/filters.jade diff --git a/views/main/index.jade b/views/main/index.jade new file mode 100644 index 0000000000..da4490409a --- /dev/null +++ b/views/main/index.jade @@ -0,0 +1,4 @@ +script(id='templates/habitrpg-main.html', type="text/ng-template") + include ./filters + div(ng-controller='TasksCtrl') + habitrpg-tasks(main='true', obj='user') diff --git a/views/options/index.jade b/views/options/index.jade index d27af6a886..c3ea045c1f 100644 --- a/views/options/index.jade +++ b/views/options/index.jade @@ -1,60 +1,64 @@ -.grid - .module.full-width - span.option-box.pull-right.wallet - include ../shared/gems +script(id='templates/habitrpg-options.html', type="text/ng-template") + .grid + .module.full-width + span.option-box.pull-right.wallet + include ../shared/gems - ul.nav.nav-tabs - li.active - a(data-toggle='tab', data-target='#profile-tab') - i.icon-user - | Profile - li - a(data-toggle='tab',data-target='#groups-tab', ng-click='fetchParty()') - i.icon-heart - | Groups - li(ng-show='user.flags.dropsEnabled') - a(data-toggle='tab',data-target='#inventory-tab') - i.icon-gift - | Inventory - li(ng-show='user.flags.dropsEnabled') - a(data-toggle='tab',data-target='#stable-tab') - i.icon-leaf - | Stable - li - a(data-toggle='tab',data-target='#tavern-tab',ng-click='fetchTavern()') - i.icon-eye-close - | Tavern - li - a(data-toggle='tab',data-target='#achievements-tab') - i.icon-certificate - | Achievements + ul.nav.nav-tabs + li.active + a(data-toggle='tab', data-target='#profile-tab') + i.icon-user + | Profile + li + a(data-toggle='tab',data-target='#groups-tab', ng-click='fetchParty()') + i.icon-heart + | Groups + li(ng-show='user.flags.dropsEnabled') + a(data-toggle='tab',data-target='#inventory-tab') + i.icon-gift + | Inventory + li(ng-show='user.flags.dropsEnabled') + a(data-toggle='tab',data-target='#stable-tab') + i.icon-leaf + | Stable + li + a(data-toggle='tab',data-target='#tavern-tab', ng-click='fetchTavern()') + i.icon-eye-close + | Tavern + li + a(data-toggle='tab',data-target='#achievements-tab') + i.icon-certificate + | Achievements + //-li + a(data-toggle='tab',data-target='#challenges-tab') + i.icon-bullhorn + | Challenges + li + a(data-toggle='tab',data-target='#settings-tab') + i.icon-wrench + | Settings - li - a(data-toggle='tab',data-target='#settings-tab') - i.icon-wrench - | Settings + .tab-content + .tab-pane.active#profile-tab + include ./profile - .tab-content - .tab-pane.active#profile-tab - include ./profile + .tab-pane#groups-tab + include ./groups/index - .tab-pane#groups-tab - include ./groups/index + .tab-pane#inventory-tab + include ./inventory - .tab-pane#inventory-tab - include ./inventory + .tab-pane#stable-tab + include ./pets - .tab-pane#stable-tab - include ./pets + .tab-pane#tavern-tab + include ./groups/tavern - .tab-pane#tavern-tab - include ./groups/tavern + .tab-pane#achievements-tab(ng-controller='UserCtrl') + include ../shared/profiles/achievements - .tab-pane#achievements-tab(ng-controller='UserCtrl') - include ../shared/profiles/achievements + .tab-pane#challenges-tab + include ./challenges/index - //
  • Challenges
  • - // app:challenges:main - - .tab-pane#settings-tab - include ./settings \ No newline at end of file + .tab-pane#settings-tab + include ./settings \ No newline at end of file diff --git a/views/tasks/index.jade b/views/shared/tasks/lists.jade similarity index 62% rename from views/tasks/index.jade rename to views/shared/tasks/lists.jade index a50a17b0cb..b43bd2df89 100644 --- a/views/tasks/index.jade +++ b/views/shared/tasks/lists.jade @@ -1,12 +1,14 @@ -div(ng-controller='TasksCtrl') - include ./filters +// Note here, we need this part of Habit to be a directive since we're going to be passing it variables from various +// parts of the app. The alternative would be to create new scopes for different containing sections, but that +// started to get unwieldy +script(id='templates/habitrpg-tasks.html', type="text/ng-template") .grid - .module(ng-controller='TasksCtrl', ng-repeat='list in taskLists', ng-class='{"rewards-module": list.type==="reward"}') + .module(ng-repeat='list in lists', ng-class='{"rewards-module": list.type==="reward"}') .task-column(class='{{list.type}}s') // Todos export/graph options - span.option-box.pull-right(ng-if='list.main && list.type=="todo"') - a.option-action(ng-show='user.history.todos', ng-click='toggleChart("todos")', tooltip='Progress') + span.option-box.pull-right(ng-if='main && list.type=="todo"') + a.option-action(ng-show='obj.history.todos', ng-click='toggleChart("todos")', tooltip='Progress') i.icon-signal //-a.option-action(ng-href='/v1/users/{{user.id}}/calendar.ics?apiToken={{user.apiToken}}', tooltip='iCal') a.option-action(ng-click='notPorted()', tooltip='iCal', ng-show='false') @@ -14,7 +16,7 @@ div(ng-controller='TasksCtrl') // // Gold & Gems - span.option-box.pull-right.wallet(ng-if='list.main && list.type=="reward"') + span.option-box.pull-right.wallet(ng-if='main && list.type=="reward"') .money | {{gold(user.stats.gp)}} span.shop_gold(tooltip='Gold') @@ -29,18 +31,18 @@ div(ng-controller='TasksCtrl') .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='list.editable', ng-hide='list.showCompleted && list.type=="todo"', data-task-type='{{list.type}}', ng-submit='addTask(list)') + 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)') 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') hr // Actual List - ul(class='{{list.type}}s', ng-show='user[list.type + "s"].length > 0', habitrpg-sortable) + ul(class='{{list.type}}s', ng-show='obj[list.type + "s"].length > 0', habitrpg-sortable) include ./task // Static Rewards - ul.items(ng-show='list.main && list.type=="reward" && user.flags.itemsEnabled') + ul.items(ng-show='main && list.type=="reward" && user.flags.itemsEnabled') li.task.reward-item(ng-hide='item.hide', ng-repeat='item in itemStore') // right-hand side control buttons .task-meta-controls @@ -59,14 +61,20 @@ div(ng-controller='TasksCtrl') br - include ./ads + // Ads + div(ng-if='main && !user.purchased.ads && list.type!="reward"') + span.pull-right + a(ng-click='modals.buyGems=true', tooltip='Remove Ads') + i.icon-remove + // Habit3 + ins.adsbygoogle(ng-init='initAds()', style='display: inline-block; width: 234px; height: 60px;', data-ad-client='ca-pub-3242350243827794', data-ad-slot='9529624576') // Todo Tabs - div(ng-if='list.type=="todo"', ng-class='{"tabbable tabs-below": list.type=="todo"}') + div(ng-if='main && list.type=="todo"', ng-class='{"tabbable tabs-below": list.type=="todo"}') button.task-action-btn.tile.spacious.bright(ng-show='list.showCompleted', ng-click='clearCompleted()') Clear Completed // remaining/completed tabs ul.nav.nav-tabs li(ng-class='{active: !list.showCompleted}') a(ng-click='list.showCompleted = false') Remaining li(ng-class='{active: list.showCompleted}') - a(ng-click='list.showCompleted= true') Complete + a(ng-click='list.showCompleted= true') Complete \ No newline at end of file diff --git a/views/tasks/task.jade b/views/shared/tasks/task.jade similarity index 80% rename from views/tasks/task.jade rename to views/shared/tasks/task.jade index c0a47fdb9b..aafb4d6bc6 100644 --- a/views/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -1,4 +1,4 @@ -li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,user.filters,user.preferences.dayStart,user.lastCron,list.showCompleted,list.main)}}', data-id='{{task.id}}') +li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', data-id='{{task.id}}') // right-hand side control buttons .task-meta-controls // Due Date @@ -12,25 +12,25 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='noTags(task.tags)') // edit - a(ng-hide='task._editing', ng-click='toggleEdit(task)', tooltip='Edit') + a(ng-hide='task._editing', ng-click='editTask(task)', tooltip='Edit') i.icon-pencil(ng-hide='task._editing') // cancel - a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel') + a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip='Cancel') i.icon-remove(ng-hide='!task._editing') //- challenges - // {{#if :task.challenge}} - // {{#if brokenChallengeLink(:task)}} - // - // {{else}} - // - // {{/}} - // {{else}} - // - // - // {{/}} + //- {{#if :task.challenge}} + //- {{#if brokenChallengeLink(:task)}} + //- + //- {{else}} + //- + //- {{/}} + //- {{else}} + //- + //- + //- {{/}} // delete - a(ng-click='remove(task)', tooltip='Delete') + a(ng-click='removeTask(list.tasks, $index)', tooltip='Delete') i.icon-trash // chart a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') @@ -43,34 +43,20 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use .task-controls.task-primary // Habits - span(ng-if='list.main && task.type=="habit"') - // only allow scoring on main tasks, not when viewing others' public tasks or when creating challenges + span(ng-if='task.type=="habit"') a.task-action-btn(ng-if='task.up', ng-click='score(task,"up")') + a.task-action-btn(ng-if='task.down', ng-click='score(task,"down")') - - //span(ng-if='!list.main') - // span.task-action-btn(ng-show='task.up') + - // span.task-action-btn(ng-show='task.down') = // Rewards - span(ng-show='list.main && task.type=="reward"') - // only allow scoring on main tasks, not when viewing others' public tasks or when creating challenges + span(ng-show='task.type=="reward"') a.money.btn-buy(ng-click='score(task, "down")') span.reward-cost {{task.value}} span.shop_gold - //span(ng-if='!list.main') - // span.money.btn-buy - // span.reward-cost {{task.value}} - // span.shop_gold // Daily & Todos span.task-checker.action-yesno(ng-if='task.type=="daily" || task.type=="todo"') - // only allow scoring on main tasks, not when viewing others' public tasks or when creating challenges - //span(ng-if='list.main') input.visuallyhidden.focusable(id='box-{{task.id}}', type='checkbox', ng-model='task.completed', ng-change='changeCheck(task)') label(for='box-{{task.id}}') - //span(ng-if='!list.main') - // input.visuallyhidden.focusable(id='box-{{task.id}}-static',type='checkbox', checked='false') - // label(for='box-{{task.id}}-static') // main content p.task-text // {{#if taskInChallenge(task)}} @@ -99,7 +85,7 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use // {{/}} // // {/} - form(ng-controller="TaskDetailsCtrl", ng-submit='save(task)') + form(ng-submit='saveTask(task)') // text & notes fieldset.option-group // {{#unless taskInChallenge(task)}} @@ -146,7 +132,7 @@ li(ng-repeat='task in user[list.type + "s"]', class='task {{taskClasses(task,use legend.option-title Due Date input.option-content.datepicker(type='text', data-date-format='mm/dd/yyyy', ng-model='task.date') - fieldset.option-group(ng-if='list.main') + fieldset.option-group legend.option-title Tags label.checkbox(ng-repeat='tag in user.tags') input(type='checkbox', ng-model='task.tags[tag.id]') diff --git a/views/tasks/ads.jade b/views/tasks/ads.jade deleted file mode 100644 index 1622a3cb74..0000000000 --- a/views/tasks/ads.jade +++ /dev/null @@ -1,25 +0,0 @@ -div(ng-if='authenticated() && !user.purchased.ads') - span.pull-right(ng-if='list.type!="reward"') - a(ng-click='modals.buyGems=true', tooltip='Remove Ads') - i.icon-remove - - div(ng-if='list.type=="habit"', habitrpg-adsense) - script(async='async', src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js') - // Habit3 - ins.adsbygoogle(style='display: inline-block; width: 234px; height: 60px;', data-ad-client='ca-pub-3242350243827794', data-ad-slot='9529624576') - script. - (adsbygoogle = window.adsbygoogle || []).push({}); - - div(ng-if='list.type=="daily"', habitrpg-adsense) - script(async='async', src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js') - // Habit3 - ins.adsbygoogle(style='display: inline-block; width: 234px; height: 60px;', data-ad-client='ca-pub-3242350243827794', data-ad-slot='9529624576') - script. - (adsbygoogle = window.adsbygoogle || []).push({}); - - div(ng-if='list.type=="todo"', habitrpg-adsense) - script(async='async', src='//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js') - // Habit3 - ins.adsbygoogle(style='display: inline-block; width: 234px; height: 60px;', data-ad-client='ca-pub-3242350243827794', data-ad-slot='9529624576') - script. - (adsbygoogle = window.adsbygoogle || []).push({}); \ No newline at end of file From fa25f3d300433aac0bd4363733cb0e5f74602d63 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sat, 26 Oct 2013 17:24:45 -0700 Subject: [PATCH 03/15] challenges: WIP beginning challenges feature. some basic CRUD & some basic subscribe / unsubscribe functional (but buggy) --- Gruntfile.js | 5 +- package.json | 2 +- public/js/controllers/challengesCtrl.js | 242 ++++++++++++++++++++++++ public/js/services/challengeServices.js | 25 +++ src/controllers/challenges.js | 158 ++++++++++++++++ src/controllers/groups.js | 8 + src/models/challenge.js | 33 ++++ src/models/group.js | 6 +- src/routes/api.js | 10 + src/server.js | 1 + views/index.jade | 10 +- views/options/challenges.html | 24 ++- views/options/challenges/index.jade | 102 ++++++++++ views/options/challenges/task.jade | 112 +++++++++++ views/options/challenges/tasks.jade | 21 ++ views/options/groups/group.jade | 39 ++-- 16 files changed, 755 insertions(+), 43 deletions(-) create mode 100644 public/js/controllers/challengesCtrl.js create mode 100644 public/js/services/challengeServices.js create mode 100644 src/controllers/challenges.js create mode 100644 src/models/challenge.js create mode 100644 views/options/challenges/index.jade create mode 100644 views/options/challenges/task.jade create mode 100644 views/options/challenges/tasks.jade diff --git a/Gruntfile.js b/Gruntfile.js index 6c0d976cb7..d44646817f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -49,6 +49,7 @@ module.exports = function(grunt) { 'public/js/services/groupServices.js', 'public/js/services/memberServices.js', 'public/js/services/guideServices.js', + 'public/js/services/challengeServices.js', 'public/js/filters/filters.js', @@ -61,14 +62,14 @@ module.exports = function(grunt) { 'public/js/controllers/settingsCtrl.js', 'public/js/controllers/statsCtrl.js', 'public/js/controllers/tasksCtrl.js', - 'public/js/controllers/taskDetailsCtrl.js', 'public/js/controllers/filtersCtrl.js', 'public/js/controllers/userCtrl.js', 'public/js/controllers/groupsCtrl.js', 'public/js/controllers/petsCtrl.js', 'public/js/controllers/inventoryCtrl.js', 'public/js/controllers/marketCtrl.js', - 'public/js/controllers/footerCtrl.js' + 'public/js/controllers/footerCtrl.js', + 'public/js/controllers/challengesCtrl.js' ] } }, diff --git a/package.json b/package.json index 847988171e..07a05f0925 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0-152", "main": "./src/server.js", "dependencies": { - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#rewrite", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared#challenges", "derby-auth": "git://github.com/lefnire/derby-auth#master", "connect-mongo": "*", "passport-facebook": "~1.0.0", diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js new file mode 100644 index 0000000000..4a882dd2f0 --- /dev/null +++ b/public/js/controllers/challengesCtrl.js @@ -0,0 +1,242 @@ +"use strict"; + +habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challenges', 'Notification', '$http', 'API_URL', '$compile', + function($scope, $rootScope, User, Challenges, Notification, $http, API_URL, $compile) { + + $http.get(API_URL + '/api/v1/groups?minimal=true').success(function(groups){ + $scope.groups = groups; + }); + + $scope.challenges = Challenges.Challenge.query(); + + //------------------------------------------------------------ + // Challenge + //------------------------------------------------------------ + + /** + * Create + */ + $scope.create = function() { + $scope.newChallenge = new Challenges.Challenge({ + name: '', + description: '', + habits: [], + dailys: [], + todos: [], + rewards: [], + leader: User.user._id, + group: null, + timestamp: +(new Date), + members: [] + }); + }; + + /** + * Save + */ + $scope.save = function(challenge) { + if (!challenge.group) return alert('Please select group'); + var isNew = !challenge._id; + challenge.$save(function(){ + if (isNew) { + Notification.text('Challenge Created'); + $scope.discard(); + Challenges.Challenge.query(); + } else { + challenge._editing = false; + } + }); + }; + + /** + * Discard + */ + $scope.discard = function() { + $scope.newChallenge = null; + }; + + + /** + * Delete + */ + $scope["delete"] = function(challenge) { + if (confirm("Delete challenge, are you sure?") !== true) return; + challenge.delete(); + }; + + //------------------------------------------------------------ + // Tasks + //------------------------------------------------------------ + + $scope.addTask = function(list) { + var task = window.habitrpgShared.helpers.taskDefaults({text: list.newTask, type: list.type}, User.user.filters); + list.tasks.unshift(task); + //User.log({op: "addTask", data: task}); //TODO persist + delete list.newTask; + }; + + $scope.removeTask = function(list, $index) { + if (confirm("Are you sure you want to delete this task?")) return; + //TODO persist + // User.log({ + // op: "delTask", + // data: task + //}); + list.splice($index, 1); + }; + + $scope.saveTask = function(task){ + task._editing = false; + // TODO persist + } + + /** + * Render graphs for user scores when the "Challenges" tab is clicked + */ + //TODO + // 1. on main tab click or party + // * sort & render graphs for party + // 2. guild -> all guilds + // 3. public -> all public +// $('#profile-challenges-tab-link').on 'shown', -> +// async.each _.toArray(model.get('groups')), (g) -> +// async.each _.toArray(g.challenges), (chal) -> +// async.each _.toArray(chal.tasks), (task) -> +// async.each _.toArray(chal.members), (member) -> +// if (history = member?["#{task.type}s"]?[task.id]?.history) and !!history +// data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) +// options = +// backgroundColor: { fill:'transparent' } +// width: 150 +// height: 50 +// chartArea: width: '80%', height: '80%' +// axisTitlePosition: 'none' +// legend: position: 'bottom' +// hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines... +// vAxis: gridlines: color: 'transparent' +// chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] +// chart.draw(data, options) + + /** + * Sync user to challenge (when they score, add to statistics) + */ + // TODO this needs to be moved to the server. Either: + // 1. Calculate on load (simplest, but bad performance) + // 2. Updated from user score API +// app.model.on("change", "_page.user.priv.tasks.*.value", function(id, value, previous, passed) { +// /* Sync to challenge, but do it later*/ +// +// var _this = this; +// return async.nextTick(function() { +// var chal, chalTask, chalUser, ctx, cu, model, pub, task, tobj; +// model = app.model; +// ctx = { +// model: model +// }; +// task = model.at("_page.user.priv.tasks." + id); +// tobj = task.get(); +// pub = model.get("_page.user.pub"); +// if (((chalTask = helpers.taskInChallenge.call(ctx, tobj)) != null) && chalTask.get()) { +// chalTask.increment("value", value - previous); +// chal = model.at("groups." + tobj.group.id + ".challenges." + tobj.challenge); +// chalUser = function() { +// return helpers.indexedAt.call(ctx, chal.path(), 'members', { +// id: pub.id +// }); +// }; +// cu = chalUser(); +// if (!(cu != null ? cu.get() : void 0)) { +// chal.push("members", { +// id: pub.id, +// name: model.get(pub.profile.name) +// }); +// cu = model.at(chalUser()); +// } else { +// cu.set('name', pub.profile.name); +// } +// return cu.set("" + tobj.type + "s." + tobj.id, { +// value: tobj.value, +// history: tobj.history +// }); +// } +// }); +// }); + + /* + -------------------------- + Unsubscribe functions + -------------------------- + */ + + $scope.taskUnsubscribe = function(e, el) { + /* + since the challenge was deleted, we don't have its data to unsubscribe from - but we have the vestiges on the task + FIXME this is a really dumb way of doing this + */ + + var deletedChal, i, path, tasks, tobj; + tasks = this.priv.get('tasks'); + tobj = tasks[$(el).attr("data-tid")]; + deletedChal = { + id: tobj.challenge, + members: [this.uid], + habits: _.where(tasks, { + type: 'habit', + challenge: tobj.challenge + }), + dailys: _.where(tasks, { + type: 'daily', + challenge: tobj.challenge + }), + todos: _.where(tasks, { + type: 'todo', + challenge: tobj.challenge + }), + rewards: _.where(tasks, { + type: 'reward', + challenge: tobj.challenge + }) + }; + switch ($(el).attr('data-action')) { + case 'keep': + this.priv.del("tasks." + tobj.id + ".challenge"); + return this.priv.del("tasks." + tobj.id + ".group"); + case 'keep-all': + return app.challenges.unsubscribe.call(this, deletedChal, true); + case 'remove': + path = "_page.lists.tasks." + this.uid + "." + tobj.type + "s"; + if (~(i = _.findIndex(this.model.get(path), { + id: tobj.id + }))) { + return this.model.remove(path, i); + } + break; + case 'remove-all': + return app.challenges.unsubscribe.call(this, deletedChal, false); + } + }; + + $scope.unsubscribe = function(keep) { + if (keep == 'cancel') { + $scope.selectedChal = undefined; + } else { + $scope.selectedChal.$leave({keep:keep}); + } + $scope.popoverEl.popover('destroy'); + } + $scope.clickUnsubscribe = function(chal, $event) { + $scope.selectedChal = chal; + $scope.popoverEl = $($event.target); + var html = $compile( + 'Remove Tasks
    \nKeep Tasks
    \nCancel
    ' + )($scope); + $scope.popoverEl.popover('destroy').popover({ + html: true, + placement: 'top', + trigger: 'manual', + title: 'Unsubscribe From Challenge And:', + content: html + }).popover('show'); + } + +}]); \ No newline at end of file diff --git a/public/js/services/challengeServices.js b/public/js/services/challengeServices.js new file mode 100644 index 0000000000..eaf4693845 --- /dev/null +++ b/public/js/services/challengeServices.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Services that persists and retrieves user from localStorage. + */ + +angular.module('challengeServices', ['ngResource']). + factory('Challenges', ['API_URL', '$resource', 'User', '$q', 'Members', + function(API_URL, $resource, User, $q, Members) { + var Challenge = $resource(API_URL + '/api/v1/challenges/:cid', + {cid:'@_id'}, + { + //'query': {method: "GET", isArray:false} + join: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/join'}, + leave: {method: "POST", url: API_URL + '/api/v1/challenges/:cid/leave'} + }); + + //var challenges = []; + + return { + Challenge: Challenge + //challenges: challenges + } + } +]); diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js new file mode 100644 index 0000000000..9ef575dfc4 --- /dev/null +++ b/src/controllers/challenges.js @@ -0,0 +1,158 @@ +// @see ../routes for routing + +var _ = require('lodash'); +var nconf = require('nconf'); +var async = require('async'); +var algos = require('habitrpg-shared/script/algos'); +var helpers = require('habitrpg-shared/script/helpers'); +var items = require('habitrpg-shared/script/items'); +var User = require('./../models/user').model; +var Group = require('./../models/group').model; +var Challenge = require('./../models/challenge').model; +var api = module.exports; + +/* + ------------------------------------------------------------------------ + Challenges + ------------------------------------------------------------------------ +*/ + +// GET +api.get = function(req, res) { + // TODO only find in user's groups (+ public) + // TODO populate (group, leader, members) + Challenge.find({},function(err, challenges){ + if(err) return res.json(500, {err:err}); + res.json(challenges); + }) +} + +// CREATE +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}); + res.json(saved); + }); +} + +// UPDATE +api.update = function(req, res){ + //FIXME sanitize + Challenge.findByIdAndUpdate(req.params.cid, {$set:req.body}, 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 + +/** + * Syncs all new tasks, deleted tasks, etc to the user object + * @param chal + * @param user + * @return nothing, user is modified directly. REMEMBER to save the user! + */ +var syncChalToUser = function(chal, user) { + if (!chal || !user) return; + + // Sync tags + var tags = user.tags || []; + var i = _.findIndex(tags, {id: chal._id}) + if (~i) { + if (tags[i].name !== chal.name) { + // update the name - it's been changed since + user.tags[i].name = chal.name; + } + } else { + user.tags.push({ + id: chal._id, + name: chal.name, + challenge: true + }); + } + tags = {}; + tags[chal._id] = true; + _.each(['habits','dailys','todos','rewards'], function(type){ + _.each(chal[type], function(task){ + _.defaults(task, { + tags: tags, + challenge: chal._id, + group: chal.group + }); + + if (~(i = _.findIndex(user[type], {id:task.id}))) { + _.defaults(user[type][i], task); + } else { + user[type].push(task); + } + }) + }) + //FIXME account for deleted tasks (each users.tasks.broken = true) +}; + +api.join = function(req, res){ + var user = res.locals.user; + var cid = req.params.cid; + + async.waterfall([ + function(cb){ + Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb); + }, + function(challenge, cb){ + if (!~user.challenges.indexOf(cid)) + user.challenges.unshift(cid); + // Add all challenge's tasks to user's tasks + syncChalToUser(challenge, user); + user.save(function(err){ + if (err) return cb(err); + cb(null, challenge); // we want the saved challenge in the return results, due to ng-resource + }); + } + ], function(err, result){ + if(err) return res.json(500,{err:err}); + res.json(result); + }); +} + +api.leave = function(req, res, next){ + var user = res.locals.user; + var cid = req.params.cid; + // whether or not to keep challenge's tasks. strictly default to true if "false" isn't provided + var keep = !(/^false$/i).test(req.query.keep); + + async.waterfall([ + function(cb){ + Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb); + }, + function(chal, cb){ + // Remove challenge from user + //User.findByIdAndUpdate(user._id, {$pull:{challenges:cid}}, cb); + var i = user.challenges.indexOf(cid) + if (~i) user.challenges.splice(i,1); + + // Remove tasks from user + _.each(chal.tasks, function(task) { + if (keep) { + delete user[task.type+'s'].id(task.id).challenge; + delete user[task.type+'s'].id(task.id).group; + } else { + user[task.type+'s'].id(task.id).remove(); + } + }); + user.save(function(err){ + if (err) return cb(err); + cb(null, chal); + }) + } + ], function(err, result){ + if(err) return res.json(500,{err:err}); + res.json(result); + }); +} \ No newline at end of file diff --git a/src/controllers/groups.js b/src/controllers/groups.js index a57268efff..cb36ffa7b0 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -41,6 +41,14 @@ api.getMember = function(req, res) { api.getGroups = function(req, res, next) { var user = res.locals.user; + // if ?minimal=true, just send down names + if (req.query.minimal) { + return Group.find({members: {'$in': [user._id]}}).select('name _id').exec(function(err, groups){ + if (err) return res.json(500, {err:err}); + res.json(groups); + }); + } + var type = req.query.type && req.query.type.split(','); // First get all groups diff --git a/src/models/challenge.js b/src/models/challenge.js new file mode 100644 index 0000000000..a74c25f39d --- /dev/null +++ b/src/models/challenge.js @@ -0,0 +1,33 @@ +var mongoose = require("mongoose"); +var Schema = mongoose.Schema; +var helpers = require('habitrpg-shared/script/helpers'); +var _ = require('lodash'); +var TaskSchema = require('./task').schema; + +var ChallengeSchema = new Schema({ + _id: {type: String, 'default': helpers.uuid}, + name: String, + description: String, + habits: [TaskSchema], + dailys: [TaskSchema], + todos: [TaskSchema], + rewards: [TaskSchema], + leader: {type: String, ref: 'User'}, + group: {type: String, ref: 'Group'}, + // FIXME remove below, we don't need it since every time we load a challenge, we'll load it with the group ref. we don't need to look up challenges by type + //type: group.type, //type: {type: String,"enum": ['guild', 'party']}, + //id: group._id + //}, + timestamp: {type: Date, 'default': Date.now}, + members: [{type: String, ref: 'User'}] +}); + +ChallengeSchema.virtual('tasks').get(function () { + var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards); + var tasks = _.object(_.pluck(tasks,'id'), tasks); + return tasks; +}); + + +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 194d164a68..b99c5870f8 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -51,13 +51,13 @@ var GroupSchema = new Schema({ balance: Number, logo: String, - leaderMessage: String + leaderMessage: String, + challenges: [{type:'String', ref:'Challenge'}] }, { - strict: 'throw', + strict: 'throw', minimize: false // So empty objects are returned }); - /** * Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration * to remove duplicates, then take these fucntions out diff --git a/src/routes/api.js b/src/routes/api.js index bd4c968ab4..7983757362 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -3,6 +3,7 @@ var router = new express.Router(); var user = require('../controllers/user'); var groups = require('../controllers/groups'); var auth = require('../controllers/auth'); +var challenges = require('../controllers/challenges'); /* ---------- /api/v1 API ------------ @@ -78,4 +79,13 @@ router.get('/members/:uid', groups.getMember); // Market router.post('/market/buy', auth.auth, user.marketBuy); +/* Challenges */ +// Note: while challenges belong to groups, and would therefore make sense as a nested resource +// (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab +// 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/join', auth.auth, challenges.join) +router.post('/challenges/:cid/leave', auth.auth, challenges.leave) + module.exports = router; \ No newline at end of file diff --git a/src/server.js b/src/server.js index cb1a0c18f0..3e9c36ebc1 100644 --- a/src/server.js +++ b/src/server.js @@ -27,6 +27,7 @@ process.on("uncaughtException", function(error) { mongoose = require('mongoose'); require('./models/user'); //load up the user schema - TODO is this necessary? require('./models/group'); +require('./models/challenge'); mongoose.connect(nconf.get('NODE_DB_URI'), function(err) { if (err) throw err; console.info('Connected with Mongoose'); diff --git a/views/index.jade b/views/index.jade index 7e93871ae9..6de28f9472 100644 --- a/views/index.jade +++ b/views/index.jade @@ -30,14 +30,14 @@ html script(type='text/javascript', src='/bower_components/jquery.cookie/jquery.cookie.js') script(type='text/javascript', src='/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js') script(type='text/javascript', src='/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js') - script(type='text/javascript', src='/bower_components/angular/angular.min.js') + script(type='text/javascript', src='/bower_components/angular/angular.js') script(type='text/javascript', src='/bower_components/angular-sanitize/angular-sanitize.min.js') script(type='text/javascript', src='/bower_components/marked/lib/marked.js') - script(type='text/javascript', src='/bower_components/angular-route/angular-route.min.js') - script(type='text/javascript', src='/bower_components/angular-resource/angular-resource.min.js') - script(type='text/javascript', src='/bower_components/angular-ui/build/angular-ui.min.js') + script(type='text/javascript', src='/bower_components/angular-route/angular-route.js') + script(type='text/javascript', src='/bower_components/angular-resource/angular-resource.js') + script(type='text/javascript', src='/bower_components/angular-ui/build/angular-ui.js') script(type='text/javascript', src='/bower_components/angular-ui-utils/modules/keypress/keypress.js') // we'll remove this once angular-bootstrap is fixed script(type='text/javascript', src='/bower_components/bootstrap/docs/assets/js/bootstrap.min.js') @@ -59,6 +59,7 @@ html script(type='text/javascript', src='/js/services/groupServices.js') script(type='text/javascript', src='/js/services/memberServices.js') script(type='text/javascript', src='/js/services/guideServices.js') + script(type='text/javascript', src='/js/services/challengeServices.js') script(type='text/javascript', src='/js/filters/filters.js') @@ -78,6 +79,7 @@ html script(type='text/javascript', src='/js/controllers/inventoryCtrl.js') script(type='text/javascript', src='/js/controllers/marketCtrl.js') script(type='text/javascript', src='/js/controllers/footerCtrl.js') + script(type='text/javascript', src='/js/controllers/challengesCtrl.js') -} //webfonts diff --git a/views/options/challenges.html b/views/options/challenges.html index edff5ba0e0..23dd937e4a 100644 --- a/views/options/challenges.html +++ b/views/options/challenges.html @@ -1,10 +1,10 @@ - +
    - <@headers> + - + {{#unless _page.party.id}} @@ -34,8 +34,9 @@ +
    - +
    @@ -46,8 +47,9 @@ {/}
    +
    - +
      @@ -128,8 +130,9 @@
    +
    - +
    {@header}
    {#each @list as :task} @@ -144,11 +147,13 @@
    {/}
    + - + Create {{@text}} Challenge + - +
    @@ -167,4 +172,5 @@ todos={_page.new.challenge.todos} rewards={_page.new.challenge.rewards} editable=true /> -
    \ No newline at end of file +
    + \ No newline at end of file diff --git a/views/options/challenges/index.jade b/views/options/challenges/index.jade new file mode 100644 index 0000000000..5e63f02efd --- /dev/null +++ b/views/options/challenges/index.jade @@ -0,0 +1,102 @@ +.row-fluid(ng-controller='ChallengesCtrl') + .span2.well + h4 Filters + ul + li + input(type='checkbox', checked='checked') + .label.label-warning todo + | Party + li + input(type='checkbox', checked='checked') + .label.label-warning todo + | (list groups) + li + input(type='checkbox', checked='checked') + .label.label-warning todo + | Subscribed + li + input(type='checkbox', checked='checked') + .label.label-warning todo + | Available + .span10 + // Creation form + a.btn.btn-success(ng-click='create()') Create Challenge + .create-challenge-from(ng-if='newChallenge') + form(ng-submit='save(newChallenge)') + div + input.btn.btn-success(type='submit', value='Save') + input.btn.btn-danger(type='button', ng-click='discard()', value='Discard') + select(ng-model='newChallenge.group', ng-required='required', name='Group', ng-options='g._id as g.name for g in groups') + .challenge-options + input.option-content(type='text', ng-model='newChallenge.name', placeholder='Challenge Title', required='required') + + habitrpg-tasks(main=false, obj='newChallenge') + + // Challenges list + .accordion-group(ng-repeat='challenge in challenges') + .accordion-heading + ul.pull-right.challenge-accordion-header-specs + li {{challenge.members.length}} Subscribers + li(ng-show='challenge.prize') + // prize + table(ng-show='challenge.prize') + tr + td {{challenge.prize}} + td + 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)') + i.icon-ban-circle + | Unsubscribe + a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()') + i.icon-ok + | Subscribe + a.accordion-toggle(data-toggle='collapse', data-target='#accordion-challenge-{{challenge._id}}') {{challenge.name}} (by {{challenge.leader.name}}) + .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') + .-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 + div(ng-if='challenge.description') {{challenge.description}} + habitrpg-tasks(obj='challenge', main=false) + h3 Statistics + div(ng-repeat='member in challenge.members') + h4 {{member.name}} + .grid + .module + app:challenges:stats(header='Habits', challenge='{{challenge}}', member='{{member}}', list='{{challenge.habits}}') + .module + app:challenges:stats(header='Dailies', challenge='{{challenge}}', member='{{member}}', list='{{challenge.dailys}}') + .module + app:challenges:stats(header='Todos', challenge='{{challenge}}', member='{{member}}', list='{{challenge.todos}}') + +//// .stats +// h5 {@header} +// div +// | {#each @list as :task} +// table +// tr +// td +// // +// FIXME commented section below isn't getting updated dynamically, temp solution is less efficient +// strong {:task.text} +// | : {challengeMemberScore(@member,:task)} +// // {round(@member[@taskType]s[:task.id].value)} +// td +// .challenge-{{@challenge.id}}-member-{{@member.id}}-history-{{:task.id}}(style='margin-left: 10px;') +// | {/} diff --git a/views/options/challenges/task.jade b/views/options/challenges/task.jade new file mode 100644 index 0000000000..8f89fbcbb7 --- /dev/null +++ b/views/options/challenges/task.jade @@ -0,0 +1,112 @@ +//- + NOTE: This file is a copy of /views/tasks/task.jade, make sure to keep them in sync! + We've cloned it because they differ widely enough that if(challenge)/else checks got out of + hand, and we needed separate controllers. + +li(ng-repeat='task in list.tasks', class='task {{taskClasses(task)}}', data-id='{{task.id}}') + .task-meta-controls + // TODO Do we want to show participants' streaks? + //- Streak + span(ng-show='task.streak') {{task.streak}} + span(tooltip='Streak Counter') + i.icon-forward + + // edit + a(ng-hide='task._editing', ng-click='toggleEdit(task)', tooltip='Edit') + i.icon-pencil(ng-hide='task._editing') + // cancel + a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel') + i.icon-remove(ng-hide='!task._editing') + + // delete + a(ng-click='remove(task)', tooltip='Delete') + i.icon-trash + // chart + a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') + i.icon-signal + // notes + span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') + i.icon-comment + + // left-hand side checkbox + .task-controls.task-primary + + // Habits + span(ng-if='task.type=="habit"') + a.task-action-btn(ng-if='task.up') + + a.task-action-btn(ng-if='task.down') - + + // Rewards + span(ng-show='task.type=="reward"') + a.money.btn-buy + span.reward-cost {{task.value}} + span.shop_gold + + // Daily & Todos + span.task-checker.action-yesno(ng-if='task.type=="daily" || task.type=="todo"') + input.visuallyhidden.focusable(id='box-{{task.id}}-static', type='checkbox', ng-model='task.completed') + label(for='box-{{task.id}}-static') + + // main content + p.task-text + | {{task.text}} + + // edit/options dialog + .task-options(ng-show='task._editing') + form(ng-submit='saveTask(task)') + // text & notes + fieldset.option-group + label.option-title Text + input.option-content(type='text', ng-model='task.text', required) + label.option-title Extra Notes + textarea.option-content(rows='3', ng-model='task.notes') + + // if Habit, plus/minus command options + fieldset.option-group(ng-if='task.type=="habit"') + legend.option-title Direction/Actions + span.task-checker.action-plusminus.select-toggle + input.visuallyhidden.focusable(id='{{task.id}}-option-plus', type='checkbox', ng-model='task.up') + label(for='{{task.id}}-option-plus') + span.task-checker.action-plusminus.select-toggle + input.visuallyhidden.focusable(id='{{task.id}}-option-minus', type='checkbox', ng-model='task.down') + label(for='{{task.id}}-option-minus') + + // if Daily, calendar + fieldset(ng-if='task.type=="daily"', class="option-group") + legend.option-title Repeat + .task-controls.tile-group.repeat-days + // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding + button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', data-day='su', ng-click='task.repeat.su = !task.repeat.su') Su + button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', data-day='m', ng-click='task.repeat.m = !task.repeat.m') M + button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', data-day='t', ng-click='task.repeat.t = !task.repeat.t') T + button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', data-day='w', ng-click='task.repeat.w = !task.repeat.w') W + button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', data-day='th', ng-click='task.repeat.th = !task.repeat.th') Th + button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', data-day='f', ng-click='task.repeat.f = !task.repeat.f') F + button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', data-day='s', ng-click='task.repeat.s = !task.repeat.s') S + + // if Reward, pricing + fieldset.option-group.option-short(ng-if='task.type=="reward"') + legend.option-title Price + input.option-content(type='number', size='16', min='0', step="any", ng-model='task.value') + .money.input-suffix + span.shop_gold + // if Todos, the due date + fieldset.option-group(ng-if='task.type=="todo"') + legend.option-title Due Date + input.option-content.datepicker(type='text', ng-model='task.date', data-date-format='mm/dd/yyyy') + + // Advanced Options + span(ng-if='task.type!="reward"') + p.option-title.mega(ng-click='task._advanced = !task._advanced') Advanced Options + fieldset.option-group.advanced-option(ng-class="{visuallyhidden: !task._advanced}") + legend.option-title + a.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title='How difficult is this task?', popover-trigger='mouseenter', popover="This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info.") + i.icon-question-sign + | Difficulty + .task-controls.tile-group.priority-multiplier(data-id='{{task.id}}') + button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!" || !task.priority}', ng-click='task.priority="!"') Easy + button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!"}', ng-click='task.priority="!!"') Medium + button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!!"}', ng-click='task.priority="!!!"') Hard + button.task-action-btn.tile.spacious(type='submit') Save & Close + + div(class='{{task.id}}-chart', ng-show='charts[task.id]') diff --git a/views/options/challenges/tasks.jade b/views/options/challenges/tasks.jade new file mode 100644 index 0000000000..0b094e5645 --- /dev/null +++ b/views/options/challenges/tasks.jade @@ -0,0 +1,21 @@ +.grid + .module(ng-repeat='list in taskLists', ng-class='{"rewards-module": list.type==="reward"}') + .task-column(class='{{list.type}}s') + + // Removed graph icon. Restore here from tasks/index.jade if needed + + // Header + h2.task-column_title {{list.header}} + + // Removed graph. Restore here from tasks/index.jade if needed + + // Add New + form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-if='challenge.leader == user._id', 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') + hr + + // Actual List + ul(class='{{list.type}}s', ng-show='list.tasks.length > 0', habitrpg-sortable) + include ./task \ No newline at end of file diff --git a/views/options/groups/group.jade b/views/options/groups/group.jade index 60e495dcf3..cc32f3b157 100644 --- a/views/options/groups/group.jade +++ b/views/options/groups/group.jade @@ -77,30 +77,21 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' input.option-content(type='text', placeholder='User Id', ng-model='invitee') input.btn(type='submit', value='Invite') - - //.accordion-group - .accordion-heading - a.accordion-toggle(data-toggle='collapse', data-parent='#accordion-{{@group.id}}-parent', href='#accordion-{{@group.id}}-challenges') Challenges - #accordion-{{@group.id}}-challenges.accordion-body.collapse - .accordion-inner - span.label - i.icon-bullhorn - | Challenges - | coming soon! - a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details - // - // {#if @group.challenges} - // - // {#each @group.challenges as :challenge} - // - // {/} - //
    - // {:challenge.name} - //
    - // Visit the Challenges for more information. - // {else} - // No challenges yet, visit the Challenges tab to create one. - // {/} + .modal(style='position: relative;top: auto;left: auto;right: auto;margin: 0 auto 20px;z-index: 1;max-width: 100%;') + .modal-header + h3 Challenges + .modal-body + a(target='_blank', href='https://trello.com/card/challenges-individual-party-guild-public/50e5d3684fe3a7266b0036d6/58') Details + div(ng-show='group.challenges') + table.table.table-striped + tr(ng-repeat='challenge in group.challenges') + td + | {{challenge.name}} + p. + Visit the Challenges for more information. + div(ng-hid='group.challenges') + p. + No challenges yet, visit the Challenges tab to create one. a.btn.btn-danger(data-id='{{group.id}}', ng-click='leave(group)') Leave From d60a56432f5460553aee61828909c166c8beb18b Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 10:38:28 -0700 Subject: [PATCH 04/15] challenges: better unlinkig of tasks on unsubscribe --- public/js/controllers/challengesCtrl.js | 55 ++------- src/controllers/challenges.js | 63 +++++++--- src/models/task.js | 7 +- src/routes/api.js | 1 + .../index.jade => challenges.jade} | 0 views/options/challenges/task.jade | 112 ----------------- views/options/challenges/tasks.jade | 21 ---- views/options/index.jade | 4 +- views/shared/tasks/task.jade | 115 ++++++++---------- 9 files changed, 112 insertions(+), 266 deletions(-) rename views/options/{challenges/index.jade => challenges.jade} (100%) delete mode 100644 views/options/challenges/task.jade delete mode 100644 views/options/challenges/tasks.jade diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index 4a882dd2f0..f43d00558e 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -168,52 +168,13 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng -------------------------- */ - $scope.taskUnsubscribe = function(e, el) { - /* - since the challenge was deleted, we don't have its data to unsubscribe from - but we have the vestiges on the task - FIXME this is a really dumb way of doing this - */ - - var deletedChal, i, path, tasks, tobj; - tasks = this.priv.get('tasks'); - tobj = tasks[$(el).attr("data-tid")]; - deletedChal = { - id: tobj.challenge, - members: [this.uid], - habits: _.where(tasks, { - type: 'habit', - challenge: tobj.challenge - }), - dailys: _.where(tasks, { - type: 'daily', - challenge: tobj.challenge - }), - todos: _.where(tasks, { - type: 'todo', - challenge: tobj.challenge - }), - rewards: _.where(tasks, { - type: 'reward', - challenge: tobj.challenge - }) - }; - switch ($(el).attr('data-action')) { - case 'keep': - this.priv.del("tasks." + tobj.id + ".challenge"); - return this.priv.del("tasks." + tobj.id + ".group"); - case 'keep-all': - return app.challenges.unsubscribe.call(this, deletedChal, true); - case 'remove': - path = "_page.lists.tasks." + this.uid + "." + tobj.type + "s"; - if (~(i = _.findIndex(this.model.get(path), { - id: tobj.id - }))) { - return this.model.remove(path, i); - } - break; - case 'remove-all': - return app.challenges.unsubscribe.call(this, deletedChal, false); - } + $scope.unlink = function(task, keep) { + // TODO move this to userServices, turn userSerivces.user into ng-resource + $http.post(API_URL + '/api/v1/user/task/' + task.id + '/unlink', {keep:keep}) + .success(function(){ + debugger + User.log({}); + }); }; $scope.unsubscribe = function(keep) { @@ -228,7 +189,7 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng $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, diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 9ef575dfc4..7b4467041e 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -81,12 +81,8 @@ var syncChalToUser = function(chal, user) { tags[chal._id] = true; _.each(['habits','dailys','todos','rewards'], function(type){ _.each(chal[type], function(task){ - _.defaults(task, { - tags: tags, - challenge: chal._id, - group: chal.group - }); - + _.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 { @@ -121,11 +117,36 @@ api.join = function(req, res){ }); } -api.leave = function(req, res, next){ +function unlink(user, cid, keep, tid) { + switch (keep) { + case 'keep': + delete user.tasks[tid].challenge; + break; + case 'remove': + user[user.tasks[tid].type+'s'].id(tid).remove(); + break; + case 'keep-all': + _.each(user.tasks, function(t){ + if (t.challenge && t.challenge.id == cid) { + delete t.challenge; + } + }); + break; + case 'remove-all': + _.each(user.tasks, function(t){ + if (t.challenge && t.challenge.id == cid) { + user[t.type+'s'].id(t.id).remove(); + } + }) + break; + } +} + +api.leave = function(req, res){ var user = res.locals.user; var cid = req.params.cid; - // whether or not to keep challenge's tasks. strictly default to true if "false" isn't provided - var keep = !(/^false$/i).test(req.query.keep); + // whether or not to keep challenge's tasks. strictly default to true if "keep-all" isn't provided + var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all'; async.waterfall([ function(cb){ @@ -136,16 +157,7 @@ api.leave = function(req, res, next){ //User.findByIdAndUpdate(user._id, {$pull:{challenges:cid}}, cb); var i = user.challenges.indexOf(cid) if (~i) user.challenges.splice(i,1); - - // Remove tasks from user - _.each(chal.tasks, function(task) { - if (keep) { - delete user[task.type+'s'].id(task.id).challenge; - delete user[task.type+'s'].id(task.id).group; - } else { - user[task.type+'s'].id(task.id).remove(); - } - }); + unlink(user, chal._id, keep) user.save(function(err){ if (err) return cb(err); cb(null, chal); @@ -155,4 +167,17 @@ api.leave = function(req, res, next){ if(err) return res.json(500,{err:err}); res.json(result); }); +} + +api.unlink = function(req, res) { + var user = res.locals.user; + var tid = req.params.id; + var cid = user.tasks[tid].challenge.id; + 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.save(function(err, saved){ + if (err) return res.json(500,{err:err}); + res.send(200); + }); } \ No newline at end of file diff --git a/src/models/task.js b/src/models/task.js index 8abdd9302e..cbaecb6491 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -25,7 +25,12 @@ var TaskSchema = new Schema({ completed: {type: Boolean, 'default': false}, priority: {type: String, 'default': '!'}, //'!!' // FIXME this should be a number or something repeat: {type: Schema.Types.Mixed, 'default': {m:1, t:1, w:1, th:1, f:1, s:1, su:1} }, - streak: {type: Number, 'default': 0} + streak: {type: Number, 'default': 0}, + challenge: { + id: {type: 'String', ref:'Challenge'}, + broken: {type: Boolean, 'default': false} + // group: {type: 'Strign', redf: 'Group'} // if we restore this, rename `id` above to `challenge` + } }); TaskSchema.methods.toJSON = function() { diff --git a/src/routes/api.js b/src/routes/api.js index 7983757362..16899b6b8a 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -37,6 +37,7 @@ router["delete"]('/user/task/:id', auth.auth, cron, verifyTaskExists, user.delet router.post('/user/task', auth.auth, cron, user.createTask); router.put('/user/task/:id/sort', auth.auth, cron, verifyTaskExists, user.sortTask); router.post('/user/clear-completed', auth.auth, cron, user.clearCompleted); +router.post('/user/task/:id/unlink', auth.auth, challenges.unlink); // removing cron since they may want to remove task first /* Items*/ router.post('/user/buy/:type', auth.auth, cron, user.buy); diff --git a/views/options/challenges/index.jade b/views/options/challenges.jade similarity index 100% rename from views/options/challenges/index.jade rename to views/options/challenges.jade diff --git a/views/options/challenges/task.jade b/views/options/challenges/task.jade deleted file mode 100644 index 8f89fbcbb7..0000000000 --- a/views/options/challenges/task.jade +++ /dev/null @@ -1,112 +0,0 @@ -//- - NOTE: This file is a copy of /views/tasks/task.jade, make sure to keep them in sync! - We've cloned it because they differ widely enough that if(challenge)/else checks got out of - hand, and we needed separate controllers. - -li(ng-repeat='task in list.tasks', class='task {{taskClasses(task)}}', data-id='{{task.id}}') - .task-meta-controls - // TODO Do we want to show participants' streaks? - //- Streak - span(ng-show='task.streak') {{task.streak}} - span(tooltip='Streak Counter') - i.icon-forward - - // edit - a(ng-hide='task._editing', ng-click='toggleEdit(task)', tooltip='Edit') - i.icon-pencil(ng-hide='task._editing') - // cancel - a(ng-hide='!task._editing', ng-click='toggleEdit(task)', tooltip='Cancel') - i.icon-remove(ng-hide='!task._editing') - - // delete - a(ng-click='remove(task)', tooltip='Delete') - i.icon-trash - // chart - a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') - i.icon-signal - // notes - span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') - i.icon-comment - - // left-hand side checkbox - .task-controls.task-primary - - // Habits - span(ng-if='task.type=="habit"') - a.task-action-btn(ng-if='task.up') + - a.task-action-btn(ng-if='task.down') - - - // Rewards - span(ng-show='task.type=="reward"') - a.money.btn-buy - span.reward-cost {{task.value}} - span.shop_gold - - // Daily & Todos - span.task-checker.action-yesno(ng-if='task.type=="daily" || task.type=="todo"') - input.visuallyhidden.focusable(id='box-{{task.id}}-static', type='checkbox', ng-model='task.completed') - label(for='box-{{task.id}}-static') - - // main content - p.task-text - | {{task.text}} - - // edit/options dialog - .task-options(ng-show='task._editing') - form(ng-submit='saveTask(task)') - // text & notes - fieldset.option-group - label.option-title Text - input.option-content(type='text', ng-model='task.text', required) - label.option-title Extra Notes - textarea.option-content(rows='3', ng-model='task.notes') - - // if Habit, plus/minus command options - fieldset.option-group(ng-if='task.type=="habit"') - legend.option-title Direction/Actions - span.task-checker.action-plusminus.select-toggle - input.visuallyhidden.focusable(id='{{task.id}}-option-plus', type='checkbox', ng-model='task.up') - label(for='{{task.id}}-option-plus') - span.task-checker.action-plusminus.select-toggle - input.visuallyhidden.focusable(id='{{task.id}}-option-minus', type='checkbox', ng-model='task.down') - label(for='{{task.id}}-option-minus') - - // if Daily, calendar - fieldset(ng-if='task.type=="daily"', class="option-group") - legend.option-title Repeat - .task-controls.tile-group.repeat-days - // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding - button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', data-day='su', ng-click='task.repeat.su = !task.repeat.su') Su - button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', data-day='m', ng-click='task.repeat.m = !task.repeat.m') M - button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', data-day='t', ng-click='task.repeat.t = !task.repeat.t') T - button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', data-day='w', ng-click='task.repeat.w = !task.repeat.w') W - button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', data-day='th', ng-click='task.repeat.th = !task.repeat.th') Th - button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', data-day='f', ng-click='task.repeat.f = !task.repeat.f') F - button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', data-day='s', ng-click='task.repeat.s = !task.repeat.s') S - - // if Reward, pricing - fieldset.option-group.option-short(ng-if='task.type=="reward"') - legend.option-title Price - input.option-content(type='number', size='16', min='0', step="any", ng-model='task.value') - .money.input-suffix - span.shop_gold - // if Todos, the due date - fieldset.option-group(ng-if='task.type=="todo"') - legend.option-title Due Date - input.option-content.datepicker(type='text', ng-model='task.date', data-date-format='mm/dd/yyyy') - - // Advanced Options - span(ng-if='task.type!="reward"') - p.option-title.mega(ng-click='task._advanced = !task._advanced') Advanced Options - fieldset.option-group.advanced-option(ng-class="{visuallyhidden: !task._advanced}") - legend.option-title - a.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title='How difficult is this task?', popover-trigger='mouseenter', popover="This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info.") - i.icon-question-sign - | Difficulty - .task-controls.tile-group.priority-multiplier(data-id='{{task.id}}') - button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!" || !task.priority}', ng-click='task.priority="!"') Easy - button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!"}', ng-click='task.priority="!!"') Medium - button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!!"}', ng-click='task.priority="!!!"') Hard - button.task-action-btn.tile.spacious(type='submit') Save & Close - - div(class='{{task.id}}-chart', ng-show='charts[task.id]') diff --git a/views/options/challenges/tasks.jade b/views/options/challenges/tasks.jade deleted file mode 100644 index 0b094e5645..0000000000 --- a/views/options/challenges/tasks.jade +++ /dev/null @@ -1,21 +0,0 @@ -.grid - .module(ng-repeat='list in taskLists', ng-class='{"rewards-module": list.type==="reward"}') - .task-column(class='{{list.type}}s') - - // Removed graph icon. Restore here from tasks/index.jade if needed - - // Header - h2.task-column_title {{list.header}} - - // Removed graph. Restore here from tasks/index.jade if needed - - // Add New - form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-if='challenge.leader == user._id', 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') - hr - - // Actual List - ul(class='{{list.type}}s', ng-show='list.tasks.length > 0', habitrpg-sortable) - include ./task \ No newline at end of file diff --git a/views/options/index.jade b/views/options/index.jade index c3ea045c1f..75d917912f 100644 --- a/views/options/index.jade +++ b/views/options/index.jade @@ -29,7 +29,7 @@ script(id='templates/habitrpg-options.html', type="text/ng-template") a(data-toggle='tab',data-target='#achievements-tab') i.icon-certificate | Achievements - //-li + li a(data-toggle='tab',data-target='#challenges-tab') i.icon-bullhorn | Challenges @@ -58,7 +58,7 @@ script(id='templates/habitrpg-options.html', type="text/ng-template") include ../shared/profiles/achievements .tab-pane#challenges-tab - include ./challenges/index + include ./challenges .tab-pane#settings-tab include ./settings \ No newline at end of file diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index aafb4d6bc6..d048e6a1c3 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -17,20 +17,15 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, // cancel a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip='Cancel') i.icon-remove(ng-hide='!task._editing') - //- challenges - //- {{#if :task.challenge}} - //- {{#if brokenChallengeLink(:task)}} - //- - //- {{else}} - //- - //- {{/}} - //- {{else}} - //- - //- - //- {{/}} + //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") + span(ng-if='!task.challenge.broken') + i.icon-bullhorn(tooltip="Challenge Task") // delete - a(ng-click='removeTask(list.tasks, $index)', tooltip='Delete') + a(ng-if='!task.challenge.id', ng-click='removeTask(list.tasks, $index)', tooltip='Delete') i.icon-trash // chart a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') @@ -59,48 +54,43 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, label(for='box-{{task.id}}') // main content p.task-text - // {{#if taskInChallenge(task)}} - // {{taskAttrFromChallenge(task,'text')}} - // {{else}} | {{task.text}} - // {{/}} // edit/options dialog .task-options(ng-show='task._editing') - // - // {#if brokenChallengeLink(:task)} - //
    - // {{#if groups[:task.group.id].challenges[:task.challenge]}} - //

    Broken Challenge Link: this task was part of a challenge, but has been removed from it. What would you like to do?

    - //

    - // Keep It |  - // Remove It - //

    - // {{else}} - //

    Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What would you like to do with these poor lil' orphans?

    - //

    - // Keep Them |  - // Remove Them - //

    - // {{/}} - //
    - // {/} + + // Broken Challenge + .well(ng-if='task.challenge.broken') + div(ng-if='task.challenge.broken==1') + 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') + 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') + 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 + |  |  + a(ng-click="unlink(task, 'remove-all')") Remove Them + form(ng-submit='saveTask(task)') // text & notes fieldset.option-group - // {{#unless taskInChallenge(task)}} label.option-title Text - input.option-content(type='text', ng-model='task.text', required) - // {{/}} + input.option-content(type='text', ng-model='task.text', required, ng-disabled='task.challenge.id') + label.option-title Extra Notes - // {{#if taskInChallenge(task)}} - // - // {{else}} - textarea.option-content(rows='3', ng-model='task.notes') - // {{/}} + textarea.option-content(rows='3', ng-model='task.notes', ng-disabled='task.challenge.id') + // if Habit, plus/minus command options - // {{#unless taskInChallenge(task)}} - fieldset.option-group(ng-if='task.type=="habit"') + fieldset.option-group(ng-if='task.type=="habit" && !task.challenge.id') legend.option-title Direction/Actions span.task-checker.action-plusminus.select-toggle input.visuallyhidden.focusable(id='{{task.id}}-option-plus', type='checkbox', ng-model='task.up') @@ -108,55 +98,52 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, span.task-checker.action-plusminus.select-toggle input.visuallyhidden.focusable(id='{{task.id}}-option-minus', type='checkbox', ng-model='task.down') label(for='{{task.id}}-option-minus') - // {{/}} + // if Daily, calendar - fieldset(ng-if='task.type=="daily"', class="option-group") + // FIXME display, but disable for challenge tasks + fieldset(ng-if='task.type=="daily" && !task.challenge.id', class="option-group") legend.option-title Repeat .task-controls.tile-group.repeat-days // note, does not use data-toggle="buttons-checkbox" - it would interfere with our own click binding - button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', data-day='su', ng-click='task.repeat.su = !task.repeat.su') Su - button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', data-day='m', ng-click='task.repeat.m = !task.repeat.m') M - button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', data-day='t', ng-click='task.repeat.t = !task.repeat.t') T - button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', data-day='w', ng-click='task.repeat.w = !task.repeat.w') W - button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', data-day='th', ng-click='task.repeat.th = !task.repeat.th') Th - button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', data-day='f', ng-click='task.repeat.f = !task.repeat.f') F - button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', data-day='s', ng-click='task.repeat.s = !task.repeat.s') S + button.task-action-btn.tile(ng-class='{active: task.repeat.su}', type='button', data-day='su', ng-click='task.repeat.su = !task.repeat.su') Su + button.task-action-btn.tile(ng-class='{active: task.repeat.m}', type='button', data-day='m', ng-click='task.repeat.m = !task.repeat.m') M + button.task-action-btn.tile(ng-class='{active: task.repeat.t}', type='button', data-day='t', ng-click='task.repeat.t = !task.repeat.t') T + button.task-action-btn.tile(ng-class='{active: task.repeat.w}', type='button', data-day='w', ng-click='task.repeat.w = !task.repeat.w') W + button.task-action-btn.tile(ng-class='{active: task.repeat.th}', type='button', data-day='th', ng-click='task.repeat.th = !task.repeat.th') Th + button.task-action-btn.tile(ng-class='{active: task.repeat.f}', type='button', data-day='f', ng-click='task.repeat.f = !task.repeat.f') F + button.task-action-btn.tile(ng-class='{active: task.repeat.s}', type='button', data-day='s', ng-click='task.repeat.s = !task.repeat.s') S + // if Reward, pricing - fieldset.option-group.option-short(ng-if='task.type=="reward"') + fieldset.option-group.option-short(ng-if='task.type=="reward" && !task.challenge.id') legend.option-title Price input.option-content(type='number', size='16', min='0', step="any", ng-model='task.value') .money.input-suffix span.shop_gold + // if Todos, the due date - fieldset.option-group(ng-if='task.type=="todo"') + fieldset.option-group(ng-if='task.type=="todo" && !task.challenge.id') legend.option-title Due Date input.option-content.datepicker(type='text', data-date-format='mm/dd/yyyy', ng-model='task.date') - fieldset.option-group + fieldset.option-group(ng-if='!task.challenge.id') legend.option-title Tags label.checkbox(ng-repeat='tag in user.tags') input(type='checkbox', ng-model='task.tags[tag.id]') | {{tag.name}} // Advanced Options - span(ng-if='task.type!="reward"') + span(ng-if='task.type!="reward"', ng-if='!task.challenge.id') p.option-title.mega(ng-click='task._advanced = !task._advanced') Advanced Options fieldset.option-group.advanced-option(ng-class="{visuallyhidden: !task._advanced}") legend.option-title a.priority-multiplier-help(href='https://trello.com/card/priority-multiplier/50e5d3684fe3a7266b0036d6/17', target='_blank', popover-title='How difficult is this task?', popover-trigger='mouseenter', popover="This multiplies its point value. Use sparingly, rely instead on our organic value-adjustment algorithms. But some tasks are grossly more valuable (Write Thesis vs Floss Teeth). Click for more info.") i.icon-question-sign | Difficulty - // {{#if taskInChallenge(task)}} - // - // {{else}} .task-controls.tile-group.priority-multiplier(data-id='{{task.id}}') button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!" || !task.priority}', ng-click='task.priority="!"') Easy button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!"}', ng-click='task.priority="!!"') Medium button.task-action-btn.tile(type='button', ng-class='{active: task.priority=="!!!"}', ng-click='task.priority="!!!"') Hard - // {{/}} - span(ng-if='task.type=="daily"') + span(ng-if='task.type=="daily" && !task.challenge.id') legend.option-title Restore Streak input.option-content(type='number', ng-model='task.streak') button.task-action-btn.tile.spacious(type='submit') Save & Close From e52d0a156a47ef319c93bb64883893b29b50259d Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 14:44:50 -0700 Subject: [PATCH 05/15] challenges: better syncing of new, updated, & deleted tasks from challenge to user. start with task.challenge.broken code --- public/js/controllers/challengesCtrl.js | 6 +- public/js/directives/directives.js | 2 - src/controllers/challenges.js | 90 ++++++++++++++++++++----- src/models/challenge.js | 1 - src/routes/api.js | 2 + views/options/challenges.jade | 24 +++---- views/shared/tasks/lists.jade | 2 +- views/shared/tasks/task.jade | 6 +- 8 files changed, 97 insertions(+), 36 deletions(-) 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 From a078889d589f32817ec798a3cf9445822f793152 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 15:27:01 -0700 Subject: [PATCH 06/15] challenges: sync score to challenge, lotsa bug fixes --- public/js/controllers/challengesCtrl.js | 53 ------------------------- public/js/controllers/tasksCtrl.js | 12 +++++- src/controllers/challenges.js | 10 +++-- src/controllers/user.js | 20 ++++++++-- src/models/task.js | 4 +- views/options/challenges.jade | 23 ++++------- views/shared/tasks/task.jade | 6 +-- 7 files changed, 45 insertions(+), 83 deletions(-) diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index 7f0e270cdf..7d9dd6b08a 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -119,50 +119,6 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng // chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] // chart.draw(data, options) - /** - * Sync user to challenge (when they score, add to statistics) - */ - // TODO this needs to be moved to the server. Either: - // 1. Calculate on load (simplest, but bad performance) - // 2. Updated from user score API -// app.model.on("change", "_page.user.priv.tasks.*.value", function(id, value, previous, passed) { -// /* Sync to challenge, but do it later*/ -// -// var _this = this; -// return async.nextTick(function() { -// var chal, chalTask, chalUser, ctx, cu, model, pub, task, tobj; -// model = app.model; -// ctx = { -// model: model -// }; -// task = model.at("_page.user.priv.tasks." + id); -// tobj = task.get(); -// pub = model.get("_page.user.pub"); -// if (((chalTask = helpers.taskInChallenge.call(ctx, tobj)) != null) && chalTask.get()) { -// chalTask.increment("value", value - previous); -// chal = model.at("groups." + tobj.group.id + ".challenges." + tobj.challenge); -// chalUser = function() { -// return helpers.indexedAt.call(ctx, chal.path(), 'members', { -// id: pub.id -// }); -// }; -// cu = chalUser(); -// if (!(cu != null ? cu.get() : void 0)) { -// chal.push("members", { -// id: pub.id, -// name: model.get(pub.profile.name) -// }); -// cu = model.at(chalUser()); -// } else { -// cu.set('name', pub.profile.name); -// } -// return cu.set("" + tobj.type + "s." + tobj.id, { -// value: tobj.value, -// history: tobj.history -// }); -// } -// }); -// }); /* -------------------------- @@ -170,15 +126,6 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng -------------------------- */ - $scope.unlink = function(task, keep) { - // TODO move this to userServices, turn userSerivces.user into ng-resource - $http.post(API_URL + '/api/v1/user/task/' + task.id + '/unlink', {keep:keep}) - .success(function(){ - debugger - User.log({}); - }); - }; - $scope.unsubscribe = function(keep) { if (keep == 'cancel') { $scope.selectedChal = undefined; diff --git a/public/js/controllers/tasksCtrl.js b/public/js/controllers/tasksCtrl.js index a33919ce0c..a4a72e8849 100644 --- a/public/js/controllers/tasksCtrl.js +++ b/public/js/controllers/tasksCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', 'Algos', 'Helpers', 'Notification', - function($scope, $rootScope, $location, User, Algos, Helpers, Notification) { +habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', 'Algos', 'Helpers', 'Notification', '$http', 'API_URL', + function($scope, $rootScope, $location, User, Algos, Helpers, Notification, $http, API_URL) { $scope.score = function(task, direction) { if (task.type === "reward" && User.user.stats.gp < task.value){ return Notification.text('Not enough GP.'); @@ -94,6 +94,14 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User', ' $scope.editing = false; }; + $scope.unlink = function(task, keep) { + // TODO move this to userServices, turn userSerivces.user into ng-resource + $http.post(API_URL + '/api/v1/user/task/' + task.id + '/unlink?keep=' + keep) + .success(function(){ + User.log({}); + }); + }; + /* ------------------------ Items diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 903f68f5b8..54af61e70a 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -139,7 +139,7 @@ var syncChalToUser = function(chal, user) { _.each(chal.tasks, function(task){ var type = task.type; _.defaults(task, {tags: tags, challenge:{}}); - _.defaults(task.challenge, {id:chal._id, broken:false}); + _.defaults(task.challenge, {id:chal._id}); if (user.tasks[task.id]) { _.merge(user.tasks[task.id], keepAttrs(task)); } else { @@ -213,8 +213,6 @@ api.leave = function(req, res){ Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb); }, function(chal, cb){ - // Remove challenge from user - //User.findByIdAndUpdate(user._id, {$pull:{challenges:cid}}, cb); var i = user.challenges.indexOf(cid) if (~i) user.challenges.splice(i,1); unlink(user, chal._id, keep) @@ -229,7 +227,11 @@ api.leave = function(req, res){ }); } -api.unlink = function(req, res) { +api.unlink = function(req, res, next) { + // they're scoring the task - commented out, we probably don't need it due to route ordering in api.js + //var urlParts = req.originalUrl.split('/'); + //if (_.contains(['up','down'], urlParts[urlParts.length -1])) return next(); + var user = res.locals.user; var tid = req.params.id; var cid = user.tasks[tid].challenge.id; diff --git a/src/controllers/user.js b/src/controllers/user.js index 54b92b20c3..bb89890807 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -1,8 +1,5 @@ /* @see ./routes.coffee for routing*/ -// fixme remove this junk, was coffeescript compiled (probably for IE8 compat) -var __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - var url = require('url'); var ipn = require('paypal-ipn'); var _ = require('lodash'); @@ -16,6 +13,7 @@ var check = validator.check; var sanitize = validator.sanitize; var User = require('./../models/user').model; var Group = require('./../models/group').model; +var Challenge = require('./../models/challenge').model; var api = module.exports; // FIXME put this in a proper location @@ -82,6 +80,16 @@ function addTask(user, task) { --------------- */ +var syncScoreToChallenge = function(task, delta){ + if (!task.challenge || !task.challenge.id) return; + Challenge.findById(task.challenge.id, function(err, chal){ + if (err) throw err; + var t = chal.tasks[task.id] + t.value += delta; + t.history.push({value: t.value, date: +new Date}); + chal.save(); + }); +} /** This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login @@ -96,6 +104,7 @@ api.scoreTask = function(req, res, next) { // Send error responses for improper API call if (!id) return res.json(500, {err: ':id required'}); if (direction !== 'up' && direction !== 'down') { + if (direction == 'unlink') return next(); return res.json(500, {err: ":direction must be 'up' or 'down'"}); } // If exists already, score it @@ -124,13 +133,16 @@ api.scoreTask = function(req, res, next) { } task = user.tasks[id]; var delta = algos.score(user, task, direction); - //user.markModified('flags'); + //user.markModified('flags'); user.save(function(err, saved) { if (err) return res.json(500, {err: err}); res.json(200, _.extend({ delta: delta }, saved.toJSON().stats)); }); + + // if it's a challenge task, sync the score + syncScoreToChallenge(task, delta); }; /** diff --git a/src/models/task.js b/src/models/task.js index cbaecb6491..6547170c3a 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -28,8 +28,8 @@ var TaskSchema = new Schema({ streak: {type: Number, 'default': 0}, challenge: { id: {type: 'String', ref:'Challenge'}, - broken: {type: Boolean, 'default': false} - // group: {type: 'Strign', redf: 'Group'} // if we restore this, rename `id` above to `challenge` + broken: String // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, etc + // group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge` } }); diff --git a/views/options/challenges.jade b/views/options/challenges.jade index 823978c81a..41408e98f2 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -2,22 +2,15 @@ .span2.well h4 Filters ul + li(ng-repeat='group in groups') + input(type='checkbox', ng-model='search.group') + | {{group.name}} li - input(type='checkbox', checked='checked') - .label.label-warning todo - | Party + input(type='checkbox', ng-model='search.members') + | Subscribed (TODO) li - input(type='checkbox', checked='checked') - .label.label-warning todo - | (list groups) - li - input(type='checkbox', checked='checked') - .label.label-warning todo - | Subscribed - li - input(type='checkbox', checked='checked') - .label.label-warning todo - | Available + input(type='checkbox', ng-model='search.members') + | Available (TODO) .span10 // Creation form a.btn.btn-success(ng-click='create()') Create Challenge @@ -33,7 +26,7 @@ habitrpg-tasks(main=false, obj='newChallenge') // Challenges list - .accordion-group(ng-repeat='challenge in challenges', ng-init='locked=true') + .accordion-group(ng-repeat='challenge in challenges | filter:search', ng-init='locked=true') .accordion-heading ul.pull-right.challenge-accordion-header-specs li {{challenge.members.length}} Subscribers diff --git a/views/shared/tasks/task.jade b/views/shared/tasks/task.jade index 3e18c28a5a..e8f62d8d0c 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -64,15 +64,15 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, 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='unlink(task, "keep")') Keep It |  |  a(ng-click="remove(list, $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 - a(ng-click="unlink(task 'keep-all')") Keep Them + a(ng-click='unlink(task, "keep-all")') Keep Them |  |  - a(ng-click="unlink(task, 'remove-all')") Remove Them + a(ng-click='unlink(task, "remove-all")') Remove Them //-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 From 44d67ac9f8d94a290106e5cea46e9a8376764575 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 17:46:21 -0700 Subject: [PATCH 07/15] UserSchema: set default profile.name (either from facebook or username) if not provided. This way we can remove the expensive username() helper function throughout our html --- src/models/user.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/models/user.js b/src/models/user.js index 74da652597..6462af039e 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -265,7 +265,16 @@ UserSchema.virtual('tasks').get(function () { // Custom setter/getter virtuals? UserSchema.pre('save', function(next) { - //this.markModified('tasks'); //FIXME + //this.markModified('tasks'); + + if (!this.profile.name) { + var fb = this.auth.facebook; + this.profile.name = + (this.auth.local && this.auth.local.username) || + (fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) || + 'Anonymous'; + } + //our own version incrementer this._v++; next(); From d62586ef0a8c5c953fe4ef42a3dcf6beefa17706 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 17:46:39 -0700 Subject: [PATCH 08/15] challenges: stats setup --- src/controllers/challenges.js | 25 +++++++++++++++++++------ views/options/challenges.jade | 26 ++------------------------ 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/src/controllers/challenges.js b/src/controllers/challenges.js index 54af61e70a..4e167ce39c 100644 --- a/src/controllers/challenges.js +++ b/src/controllers/challenges.js @@ -19,12 +19,25 @@ var api = module.exports; // GET api.get = function(req, res) { - // TODO only find in user's groups (+ public) - // TODO populate (group, leader, members) - Challenge.find({},function(err, challenges){ - if(err) return res.json(500, {err:err}); - res.json(challenges); - }) + var user = res.locals.user; + Challenge.find({$or:[{leader: user._id}, {members:{$in:[user._id]}}]}) + .populate('members', 'profile.name habits dailys rewards todos') + .exec(function(err, challenges){ + if(err) return res.json(500, {err:err}); + + // slim down the return members' tasks to only the ones in the challenge + _.each(challenges, function(challenge){ + _.each(challenge.members, function(member){ + _.each(['habits', 'dailys', 'todos', 'rewards'], function(type){ + member[type] = _.where(member[type], function(task){ + return task.challenge && task.challenge.id == challenge._id; + }) + }) + }) + }); + + res.json(challenges); + }) } // CREATE diff --git a/views/options/challenges.jade b/views/options/challenges.jade index 41408e98f2..d3710d1166 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -69,27 +69,5 @@ habitrpg-tasks(obj='challenge', main=false) h3 Statistics div(ng-repeat='member in challenge.members') - h4 {{member.name}} - .grid - .module - app:challenges:stats(header='Habits', challenge='{{challenge}}', member='{{member}}', list='{{challenge.habits}}') - .module - app:challenges:stats(header='Dailies', challenge='{{challenge}}', member='{{member}}', list='{{challenge.dailys}}') - .module - app:challenges:stats(header='Todos', challenge='{{challenge}}', member='{{member}}', list='{{challenge.todos}}') - -//// .stats -// h5 {@header} -// div -// | {#each @list as :task} -// table -// tr -// td -// // -// FIXME commented section below isn't getting updated dynamically, temp solution is less efficient -// strong {:task.text} -// | : {challengeMemberScore(@member,:task)} -// // {round(@member[@taskType]s[:task.id].value)} -// td -// .challenge-{{@challenge.id}}-member-{{@member.id}}-history-{{:task.id}}(style='margin-left: 10px;') -// | {/} + h4 {{member.profile.name}} + habitrpg-tasks(main=false, obj='member') \ No newline at end of file From f178eb2322de897d0a6a08b192c0172b5d8b6a45 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 18:10:22 -0700 Subject: [PATCH 09/15] challenges: better lock-down on member's tasks (remove actionable items except stats) --- public/js/controllers/challengesCtrl.js | 2 +- views/options/challenges.jade | 14 ++++----- views/shared/tasks/lists.jade | 2 +- views/shared/tasks/task.jade | 40 +++++++++++++------------ 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index 7d9dd6b08a..9a716e0e05 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -45,7 +45,7 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng } else { // TODO figure out a more elegant way about this //challenge._editing = false; - $scope.locked = true; + challenge._locked = true; } }); }; diff --git a/views/options/challenges.jade b/views/options/challenges.jade index d3710d1166..a463456d2d 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -26,7 +26,7 @@ habitrpg-tasks(main=false, obj='newChallenge') // Challenges list - .accordion-group(ng-repeat='challenge in challenges | filter:search', ng-init='locked=true') + .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 @@ -51,14 +51,14 @@ .accordion-inner // Edit button ul.unstyled() - li(ng-show='challenge.leader==user._id && locked') - button.btn.btn-default(ng-click='locked = false') Edit - li(ng-hide='locked') + li(ng-show='challenge.leader==user._id && challenge._locked') + 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-default(ng-click='locked=true') Cancel + button.btn.btn-default(ng-click='challenge._locked=true') Cancel - div(ng-hide='locked') + div(ng-hide='challenge._locked') .-options input.option-content(type='text', ng-model='challenge.name') textarea.option-content(cols='3', placeholder='Description', ng-model='challenge.description') @@ -68,6 +68,6 @@ div(ng-if='challenge.description') {{challenge.description}} habitrpg-tasks(obj='challenge', main=false) h3 Statistics - div(ng-repeat='member in challenge.members') + div(ng-repeat='member in challenge.members', ng-init='member._locked = true') h4 {{member.profile.name}} habitrpg-tasks(main=false, obj='member') \ No newline at end of file diff --git a/views/shared/tasks/lists.jade b/views/shared/tasks/lists.jade index 4d38ddccfc..daf81e96ed 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-hide='locked || (list.showCompleted && list.type=="todo")', ng-submit='addTask(list)') + form.addtask-form.form-inline.new-task-form(name='new{{list.type}}form', ng-hide='obj._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 e8f62d8d0c..3961ab5700 100644 --- a/views/shared/tasks/task.jade +++ b/views/shared/tasks/task.jade @@ -1,6 +1,7 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main)}}', data-id='{{task.id}}') // right-hand side control buttons .task-meta-controls + // Due Date span.task-date(ng-if='task.type=="todo" && task.date') | {{task.date}} @@ -9,26 +10,27 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, span(tooltip='Streak Counter') i.icon-forward - i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='noTags(task.tags)') + // Icons only available if you own the tasks (aka, hidden from challenge stats) + span(ng-if='!obj._locked') + i.icon-tags(tooltip='{{appliedTags(user.tags, task.tags)}}', ng-hide='noTags(task.tags)') + // edit + a(ng-hide='task._editing', ng-click='editTask(task)', tooltip='Edit') + i.icon-pencil(ng-hide='task._editing') + // cancel + a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip='Cancel') + i.icon-remove(ng-hide='!task._editing') + //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") + span(ng-if='!task.challenge.broken') + i.icon-bullhorn(tooltip="Challenge Task") + // delete + a(ng-if='!task.challenge.id', ng-click='removeTask(list.tasks, $index)', tooltip='Delete') + i.icon-trash - // edit - a(ng-hide='task._editing', ng-click='editTask(task)', tooltip='Edit') - i.icon-pencil(ng-hide='task._editing') - // cancel - a(ng-hide='!task._editing', ng-click='editTask(task)', tooltip='Cancel') - i.icon-remove(ng-hide='!task._editing') - //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") - span(ng-if='!task.challenge.broken') - i.icon-bullhorn(tooltip="Challenge Task") - - // delete - a(ng-if='!task.challenge.id', ng-click='removeTask(list.tasks, $index)', tooltip='Delete') - i.icon-trash // chart - a(ng-show='task.history', ng-click='toggleChart(task.id, task)', tooltip='Progress') + a(ng-show='task.history', ng-click='toggleChart(obj._id+task.id, task)', tooltip='Progress') i.icon-signal // notes span.task-notes(ng-show='task.notes && !task._editing', popover-trigger='mouseenter', popover-placement='left', popover='{{task.notes}}', popover-title='{{task.text}}') @@ -148,4 +150,4 @@ li(ng-repeat='task in list.tasks', class='task {{taskClasses(task, user.filters, input.option-content(type='number', ng-model='task.streak') button.task-action-btn.tile.spacious(type='submit') Save & Close - div(class='{{task.id}}-chart', ng-show='charts[task.id]') + div(class='{{obj._id}}{{task.id}}-chart', ng-show='charts[obj._id+task.id]') From 35c4a62de037eccb7dd0009fc6d003b7e3a36110 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Sun, 27 Oct 2013 22:27:07 -0700 Subject: [PATCH 10/15] challenges: switch from ngRoute to ui-router --- bower.json | 6 +- public/js/app.js | 118 ++++++++++- public/js/controllers/challengesCtrl.js | 92 +++------ public/js/controllers/groupsCtrl.js | 12 +- public/js/controllers/rootCtrl.js | 8 +- public/js/services/groupServices.js | 4 +- src/models/challenge.js | 2 + src/models/group.js | 35 +--- src/models/task.js | 2 + views/index.jade | 3 +- views/main/index.jade | 2 +- views/options/challenges.html | 176 ---------------- views/options/challenges.jade | 139 ++++++------- views/options/groups/index.jade | 131 ++++++------ views/options/groups/tavern.jade | 2 +- views/options/index.jade | 65 ++---- views/options/inventory.jade | 54 ----- views/options/inventory/index.jade | 15 ++ views/options/inventory/inventory.jade | 55 +++++ views/options/inventory/stable.jade | 30 +++ views/options/pets.jade | 29 --- views/options/profile.jade | 259 +++++++++++++----------- views/options/settings.jade | 83 ++++---- 23 files changed, 614 insertions(+), 708 deletions(-) delete mode 100644 views/options/challenges.html delete mode 100644 views/options/inventory.jade create mode 100644 views/options/inventory/index.jade create mode 100644 views/options/inventory/inventory.jade create mode 100644 views/options/inventory/stable.jade delete mode 100644 views/options/pets.jade diff --git a/bower.json b/bower.json index 37542ee3e8..5150bf06b2 100644 --- a/bower.json +++ b/bower.json @@ -19,7 +19,7 @@ "angular-resource": "1.2.0-rc.2", "angular-ui": "~0.4.0", "angular-bootstrap": "~0.5.0", - "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#rewrite", + "habitrpg-shared": "git://github.com/HabitRPG/habitrpg-shared.git#challenges", "bootstrap-tour": "0.5.0", "BrowserQuest": "https://github.com/mozilla/BrowserQuest.git", "github-buttons": "git://github.com/mdo/github-buttons.git", @@ -29,11 +29,11 @@ "sticky": "*", "bootstrap-datepicker": "~1.2.0", "bootstrap": "v2.3.2", - "angular-route": "1.2.0-rc.2", "angular-ui-utils": "~0.0.4", "angular-sanitize": "1.2.0-rc.2", "marked": "~0.2.9", - "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master" + "JavaScriptButtons": "git://github.com/paypal/JavaScriptButtons.git#master", + "angular-ui-router": "eda67d2da08cbe2aa1aa39ef154a87c7afe480ec" }, "resolutions": { "jquery": "~2.0.3", diff --git a/public/js/app.js b/public/js/app.js index 73936bae9a..9c38ba99dd 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1,21 +1,123 @@ "use strict"; window.habitrpg = angular.module('habitrpg', - ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices', 'sharedServices', 'authServices', 'notificationServices', 'guideServices', 'ui.bootstrap', 'ui.keypress']) + ['ngRoute', 'ngResource', 'ngSanitize', 'userServices', 'groupServices', 'memberServices', 'challengeServices', 'sharedServices', 'authServices', 'notificationServices', 'guideServices', 'ui.bootstrap', 'ui.keypress', 'ui.router']) .constant("API_URL", "") .constant("STORAGE_USER_ID", 'habitrpg-user') .constant("STORAGE_SETTINGS_ID", 'habit-mobile-settings') //.constant("STORAGE_GROUPS_ID", "") // if we decide to take groups offline - .config(['$routeProvider', '$httpProvider', 'STORAGE_SETTINGS_ID', - function($routeProvider, $httpProvider, STORAGE_SETTINGS_ID) { - $routeProvider - //.when('/login', {templateUrl: 'views/login.html'}) - .when('/tasks', {templateUrl: 'templates/habitrpg-main.html'}) - .when('/options', {templateUrl: 'templates/habitrpg-options.html'}) + .config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'STORAGE_SETTINGS_ID', + function($stateProvider, $urlRouterProvider, $httpProvider, STORAGE_SETTINGS_ID) { - .otherwise({redirectTo: '/tasks'}); + $urlRouterProvider + // Setup default selected tabs + .when('/options', '/options/profile/avatar') + .when('/options/profile', '/options/profile/avatar') + .when('/options/groups', '/options/groups/tavern') + .when('/options/inventory', '/options/inventory/inventory') + + // redirect states that don't match + .otherwise("/tasks"); + + $stateProvider + + // Tasks + .state('tasks', { + url: "/tasks", + templateUrl: "partials/main.html" + }) + + // Options + .state('options', { + url: "/options", + templateUrl: "partials/options.html", + controller: function(){} + }) + + // Options > Profile + .state('options.profile', { + url: "/profile", + templateUrl: "partials/options.profile.html", + controller: 'UserCtrl' + }) + .state('options.profile.avatar', { + url: "/avatar", + templateUrl: "partials/options.profile.avatar.html" + }) + .state('options.profile.stats', { + url: "/stats", + templateUrl: "partials/options.profile.stats.html" + }) + .state('options.profile.profile', { + url: "/stats", + templateUrl: "partials/options.profile.profile.html" + }) + + // Options > Groups + .state('options.groups', { + url: "/groups", + templateUrl: "partials/options.groups.html" + }) + .state('options.groups.tavern', { + url: "/tavern", + templateUrl: "partials/options.groups.tavern.html", + controller: 'TavernCtrl' + // TODO this doesn't work, seems ngResource doesn't get the .then() function +// resolve: { +// group: ['Groups', function(Groups){ +// //return Groups.fetchTavern(); +// }] +// } + }) + .state('options.groups.party', { + url: '/party', + templateUrl: "partials/options.groups.party.html", + controller: 'PartyCtrl' + }) + .state('options.groups.guilds', { + url: '/party', + templateUrl: "partials/options.groups.guilds.html", + controller: 'GuildsCtrl' + }) + + // Options > Inventory + .state('options.inventory', { + url: '/inventory', + templateUrl: "partials/options.inventory.html" + }) + .state('options.inventory.inventory', { + url: '/inventory', + templateUrl: "partials/options.inventory.inventory.html" + }) + .state('options.inventory.stable', { + url: '/stable', + templateUrl: "partials/options.inventory.stable.html" + }) + + // Options > Challenges + .state('options.challenges', { + url: "/challenges", + controller: 'ChallengesCtrl', + templateUrl: "partials/options.challenges.html", + resolve: { + groups: ['$http', 'API_URL', function($http, API_URL){ + // TODO come up with more unified ngResource-style approach + return $http.get(API_URL + '/api/v1/groups?minimal=true'); + }], + challenges: ['Challenges', function(Challenges){ + return Challenges.Challenge.query(); + }] + } + }) + + // Options > Settings + .state('options.settings', { + url: "/settings", + controller: 'SettingsCtrl', + templateUrl: "partials/options.settings.html" + }) var settings = JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID)); if (settings && settings.auth) { diff --git a/public/js/controllers/challengesCtrl.js b/public/js/controllers/challengesCtrl.js index 9a716e0e05..264bc1c5f9 100644 --- a/public/js/controllers/challengesCtrl.js +++ b/public/js/controllers/challengesCtrl.js @@ -1,13 +1,11 @@ "use strict"; -habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challenges', 'Notification', '$http', 'API_URL', '$compile', - function($scope, $rootScope, User, Challenges, Notification, $http, API_URL, $compile) { +habitrpg.controller("ChallengesCtrl", ['$scope', 'User', 'Challenges', 'Notification', '$compile', 'groups', 'challenges', + function($scope, User, Challenges, Notification, $compile, groups, challenges) { - $http.get(API_URL + '/api/v1/groups?minimal=true').success(function(groups){ - $scope.groups = groups; - }); - - $scope.challenges = Challenges.Challenge.query(); + // groups & challenges are loaded as `resolve` via ui-router (see app.js) + $scope.groups = groups; + $scope.challenges = challenges; //------------------------------------------------------------ // Challenge @@ -92,61 +90,33 @@ habitrpg.controller("ChallengesCtrl", ['$scope', '$rootScope', 'User', 'Challeng // TODO persist } - /** - * Render graphs for user scores when the "Challenges" tab is clicked - */ - //TODO - // 1. on main tab click or party - // * sort & render graphs for party - // 2. guild -> all guilds - // 3. public -> all public -// $('#profile-challenges-tab-link').on 'shown', -> -// async.each _.toArray(model.get('groups')), (g) -> -// async.each _.toArray(g.challenges), (chal) -> -// async.each _.toArray(chal.tasks), (task) -> -// async.each _.toArray(chal.members), (member) -> -// if (history = member?["#{task.type}s"]?[task.id]?.history) and !!history -// data = google.visualization.arrayToDataTable _.map(history, (h)-> [h.date,h.value]) -// options = -// backgroundColor: { fill:'transparent' } -// width: 150 -// height: 50 -// chartArea: width: '80%', height: '80%' -// axisTitlePosition: 'none' -// legend: position: 'bottom' -// hAxis: gridlines: color: 'transparent' # since you can't seem to *remove* gridlines... -// vAxis: gridlines: color: 'transparent' -// chart = new google.visualization.LineChart $(".challenge-#{chal.id}-member-#{member.id}-history-#{task.id}")[0] -// chart.draw(data, options) + /* + -------------------------- + Unsubscribe functions + -------------------------- + */ - - /* - -------------------------- - Unsubscribe functions - -------------------------- - */ - - $scope.unsubscribe = function(keep) { - if (keep == 'cancel') { - $scope.selectedChal = undefined; - } else { - $scope.selectedChal.$leave({keep:keep}); - } - $scope.popoverEl.popover('destroy'); - } - $scope.clickUnsubscribe = function(chal, $event) { - $scope.selectedChal = chal; - $scope.popoverEl = $($event.target); - var html = $compile( - 'Remove Tasks
    \nKeep Tasks
    \nCancel
    ' - )($scope); - $scope.popoverEl.popover('destroy').popover({ - html: true, - placement: 'top', - trigger: 'manual', - title: 'Unsubscribe From Challenge And:', - content: html - }).popover('show'); + $scope.unsubscribe = function(keep) { + if (keep == 'cancel') { + $scope.selectedChal = undefined; + } else { + $scope.selectedChal.$leave({keep:keep}); } + $scope.popoverEl.popover('destroy'); + } + $scope.clickUnsubscribe = function(chal, $event) { + $scope.selectedChal = chal; + $scope.popoverEl = $($event.target); + var html = $compile( + 'Remove Tasks
    \nKeep Tasks
    \nCancel
    ' + )($scope); + $scope.popoverEl.popover('destroy').popover({ + html: true, + placement: 'top', + trigger: 'manual', + title: 'Unsubscribe From Challenge And:', + content: html + }).popover('show'); + } }]); \ No newline at end of file diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index a4f487f109..b0d1fba4e8 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -1,7 +1,7 @@ "use strict"; -habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'API_URL', '$q', 'User', 'Members', '$location', - function($scope, $rootScope, Groups, $http, API_URL, $q, User, Members, $location) { +habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'API_URL', '$q', 'User', 'Members', '$location', '$state', + function($scope, $rootScope, Groups, $http, API_URL, $q, User, Members, $location, $state) { $scope.isMember = function(user, group){ return ~(group.members.indexOf(user._id)); @@ -25,10 +25,10 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A $scope.clickMember = function(uid, forceShow) { if (User.user._id == uid && !forceShow) { - if ($location.path() == '/tasks') { - $location.path('/options'); + if ($state.is('tasks')) { + $state.go('options'); } else { - $location.path('/tasks'); + $state.go('tasks'); } } else { // We need the member information up top here, but then we pass it down to the modal controller @@ -117,6 +117,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A .controller("GuildsCtrl", ['$scope', 'Groups', 'User', '$rootScope', function($scope, Groups, User, $rootScope) { + Groups.fetchGuilds(); $scope.type = 'guild'; $scope.text = 'Guild'; $scope.newGroup = new Groups.Group({type:'guild', privacy:'private', leader: User.user._id, members: [User.user._id]}); @@ -202,6 +203,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A .controller("TavernCtrl", ['$scope', 'Groups', 'User', function($scope, Groups, User) { + Groups.fetchTavern(); $scope.group = Groups.groups.tavern; $scope.rest = function(){ diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 1ad39e2285..3c4658d764 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -3,8 +3,8 @@ /* Make user and settings available for everyone through root scope. */ -habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', - function($scope, $rootScope, $location, User, $http) { +habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$http', '$state', '$stateParams', + function($scope, $rootScope, $location, User, $http, $state, $stateParams) { $rootScope.modals = {}; $rootScope.modals.achievements = {}; $rootScope.User = User; @@ -12,6 +12,10 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ $rootScope.settings = User.settings; $rootScope.flash = {errors: [], warnings: []}; + // Angular UI Router + $rootScope.$state = $state; + $rootScope.$stateParams = $stateParams; + // indexOf helper $scope.indexOf = function(haystack, needle){ return ~haystack.indexOf(needle); diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 7e441b1839..47c1b610d8 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -44,9 +44,9 @@ angular.module('groupServices', ['ngResource']). fetchGuilds: _.once(function(){ $('#loading-indicator').show(); Group.query({type:'guilds'}, function(_groups){ + $('#loading-indicator').hide(); guildsQ.resolve(_groups); Members.populate(_groups); - $('#loading-indicator').hide(); }) Group.query({type:'public'}, function(_groups){ publicQ.resolve(_groups); @@ -58,8 +58,8 @@ angular.module('groupServices', ['ngResource']). $('#loading-indicator').show(); Group.query({type:'tavern'}, function(_groups){ $('#loading-indicator').hide(); - tavernQ.resolve(_groups[0]); Members.populate(_groups[0]); + tavernQ.resolve(_groups[0]); }) }), diff --git a/src/models/challenge.js b/src/models/challenge.js index 0631e648f4..4971dcc992 100644 --- a/src/models/challenge.js +++ b/src/models/challenge.js @@ -20,6 +20,8 @@ var ChallengeSchema = new Schema({ //}, timestamp: {type: Date, 'default': Date.now}, members: [{type: String, ref: 'User'}] +}, { + minimize: 'false' }); ChallengeSchema.virtual('tasks').get(function () { diff --git a/src/models/group.js b/src/models/group.js index b99c5870f8..6aed645bea 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -7,37 +7,14 @@ var GroupSchema = new Schema({ _id: {type: String, 'default': helpers.uuid}, name: String, description: String, - leader: { - type: String, - ref: 'User' - }, - members: [ - { - type: String, - ref: 'User' - } - ], - invites: [ - { - type: String, - ref: 'User' - } - ], - type: { - type: String, - "enum": ['guild', 'party'] - }, - privacy: { - type: String, - "enum": ['private', 'public'] - }, - _v: { - Number: Number, - 'default': 0 - }, + leader: {type: String, ref: 'User'}, + members: [{type: String, ref: 'User'}], + invites: [{type: String, ref: 'User'}], + type: {type: String, "enum": ['guild', 'party']}, + privacy: {type: String, "enum": ['private', 'public']}, + _v: {Number: Number,'default': 0}, websites: Array, chat: Array, - /* # [{ # timestamp: Date diff --git a/src/models/task.js b/src/models/task.js index 6547170c3a..fd89af35a4 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -31,6 +31,8 @@ var TaskSchema = new Schema({ broken: String // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, etc // group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge` } +}, { + minimize: 'false' }); TaskSchema.methods.toJSON = function() { diff --git a/views/index.jade b/views/index.jade index 6de28f9472..4ea8d9a182 100644 --- a/views/index.jade +++ b/views/index.jade @@ -31,6 +31,7 @@ html script(type='text/javascript', src='/bower_components/bootstrap-growl/jquery.bootstrap-growl.min.js') script(type='text/javascript', src='/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js') script(type='text/javascript', src='/bower_components/angular/angular.js') + script(type='text/javascript', src='/bower_components/angular-ui-router/release/angular-ui-router.js') script(type='text/javascript', src='/bower_components/angular-sanitize/angular-sanitize.min.js') script(type='text/javascript', src='/bower_components/marked/lib/marked.js') @@ -109,7 +110,7 @@ html .exp-chart(ng-show='charts.exp') - #main(ng-view) + #main(ui-view) include ./shared/footer diff --git a/views/main/index.jade b/views/main/index.jade index da4490409a..586ce3ca60 100644 --- a/views/main/index.jade +++ b/views/main/index.jade @@ -1,4 +1,4 @@ -script(id='templates/habitrpg-main.html', type="text/ng-template") +script(id='partials/main.html', type="text/ng-template") include ./filters div(ng-controller='TasksCtrl') habitrpg-tasks(main='true', obj='user') diff --git a/views/options/challenges.html b/views/options/challenges.html deleted file mode 100644 index 23dd937e4a..0000000000 --- a/views/options/challenges.html +++ /dev/null @@ -1,176 +0,0 @@ -
    - - - - - - - - - {{#unless _page.party.id}} - Join a party first. - {{else}} - - {{/}} - - - - -
    - {{#each _page.guilds as :guild}} -
    - -
    - {{/}} -
    -
    - - - - - -
    -
    - - -
    - -
    -
    - - {#each @list as :challenge} - - {/} -
    -
    -
    - - -
    -
    -
      -
    • - {count(@challenge.members)} Subscribers -
    • -
    • - - {#if @challenge.prize} -
      {@challenge.prize} Prize
      - {/} -
    • -
    • - - {#with @challenge} - Unsubscribe - Subscribe - {/} -
    • -
    - {@challenge.name} (by {@challenge.user.name}) - - -
    -
    -
    - - - - {#if and(not(_page.editing.challenges[@challenge.id]),equal(@challenge.user.uid,_session.userId))} - - {else} - - {/} - - - {#if _page.editing.challenges[@challenge.id]} -
    - - - -
    - {{#with @challenge}} - Delete - {{/}} - {/} - {#if @challenge.description}
    {@challenge.description}
    {/} - -
    - -
    - -

    Statistics

    - {#each @challenge.members as :member} -

    {:member.name}

    -
    -
    - -
    -
    - -
    -
    - -
    -
    - {/} -
    -
    -
    -
    - - -
    {@header}
    -
    - {#each @list as :task} - - - -
    - - {:task.text}: {challengeMemberScore(@member,:task)} - -
    -
    - {/} -
    -
    - - - Create {{@text}} Challenge - - - - -
    - - -
    - -
    - -
    - - -
    - -
    -
    \ No newline at end of file diff --git a/views/options/challenges.jade b/views/options/challenges.jade index a463456d2d..2abca59c4d 100644 --- a/views/options/challenges.jade +++ b/views/options/challenges.jade @@ -1,73 +1,74 @@ -.row-fluid(ng-controller='ChallengesCtrl') - .span2.well - h4 Filters - ul - li(ng-repeat='group in groups') - input(type='checkbox', ng-model='search.group') - | {{group.name}} - li - input(type='checkbox', ng-model='search.members') - | Subscribed (TODO) - li - input(type='checkbox', ng-model='search.members') - | Available (TODO) - .span10 - // Creation form - a.btn.btn-success(ng-click='create()') Create Challenge - .create-challenge-from(ng-if='newChallenge') - form(ng-submit='save(newChallenge)') - div - input.btn.btn-success(type='submit', value='Save') - input.btn.btn-danger(type='button', ng-click='discard()', value='Discard') - select(ng-model='newChallenge.group', ng-required='required', name='Group', ng-options='g._id as g.name for g in groups') - .challenge-options - input.option-content(type='text', ng-model='newChallenge.name', placeholder='Challenge Title', required='required') +script(type='text/ng-template', id='partials/options.challenges.html') + .row-fluid + .span2.well + h4 Filters + ul + li(ng-repeat='group in groups') + input(type='checkbox', ng-model='search.group') + | {{group.name}} + li + input(type='checkbox', ng-model='search.members') + | Subscribed (TODO) + li + input(type='checkbox', ng-model='search.members') + | Available (TODO) + .span10 + // Creation form + a.btn.btn-success(ng-click='create()') Create Challenge + .create-challenge-from(ng-if='newChallenge') + form(ng-submit='save(newChallenge)') + div + input.btn.btn-success(type='submit', value='Save') + input.btn.btn-danger(type='button', ng-click='discard()', value='Discard') + select(ng-model='newChallenge.group', ng-required='required', name='Group', ng-options='g._id as g.name for g in groups') + .challenge-options + input.option-content(type='text', ng-model='newChallenge.name', placeholder='Challenge Title', required='required') - habitrpg-tasks(main=false, obj='newChallenge') + habitrpg-tasks(main=false, obj='newChallenge') - // Challenges list - .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(ng-show='challenge.prize') - // prize - table(ng-show='challenge.prize') - tr - td {{challenge.prize}} - td - 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)') - i.icon-ban-circle - | Unsubscribe - a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()') - i.icon-ok - | Subscribe - a.accordion-toggle(data-toggle='collapse', data-target='#accordion-challenge-{{challenge._id}}') {{challenge.name}} (by {{challenge.leader.name}}) - .accordion-body.collapse(id='accordion-challenge-{{challenge._id}}') - .accordion-inner - // Edit button - ul.unstyled() - li(ng-show='challenge.leader==user._id && challenge._locked') - 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-default(ng-click='challenge._locked=true') Cancel + // Challenges list + .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(ng-show='challenge.prize') + // prize + table(ng-show='challenge.prize') + tr + td {{challenge.prize}} + td + 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)') + i.icon-ban-circle + | Unsubscribe + a.btn.btn-small.btn-success(ng-hide='indexOf(challenge.members, user._id)', ng-click='challenge.$join()') + i.icon-ok + | Subscribe + a.accordion-toggle(data-toggle='collapse', data-target='#accordion-challenge-{{challenge._id}}') {{challenge.name}} (by {{challenge.leader.name}}) + .accordion-body.collapse(id='accordion-challenge-{{challenge._id}}') + .accordion-inner + // Edit button + ul.unstyled() + li(ng-show='challenge.leader==user._id && challenge._locked') + 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-default(ng-click='challenge._locked=true') Cancel - div(ng-hide='challenge._locked') - .-options - input.option-content(type='text', ng-model='challenge.name') - textarea.option-content(cols='3', placeholder='Description', ng-model='challenge.description') - // - hr + div(ng-hide='challenge._locked') + .-options + input.option-content(type='text', ng-model='challenge.name') + textarea.option-content(cols='3', placeholder='Description', ng-model='challenge.description') + // + hr - div(ng-if='challenge.description') {{challenge.description}} - habitrpg-tasks(obj='challenge', main=false) - h3 Statistics - div(ng-repeat='member in challenge.members', ng-init='member._locked = true') - h4 {{member.profile.name}} - habitrpg-tasks(main=false, obj='member') \ No newline at end of file + div(ng-if='challenge.description') {{challenge.description}} + habitrpg-tasks(obj='challenge', main=false) + h3 Statistics + div(ng-repeat='member in challenge.members', ng-init='member._locked = true') + h4 {{member.profile.name}} + habitrpg-tasks(main=false, obj='member') \ No newline at end of file diff --git a/views/options/groups/index.jade b/views/options/groups/index.jade index 566a25295f..6481d5e79f 100644 --- a/views/options/groups/index.jade +++ b/views/options/groups/index.jade @@ -1,66 +1,79 @@ // FIXME note, due to https://github.com/angular-ui/bootstrap/issues/783 we can't use nested angular-bootstrap tabs // Subscribe to that ticket & change this when they fix -ul.nav.nav-tabs - li.active - a(data-target='#groups-party', data-toggle='tab') Party - li - a(data-target='#groups-guilds', data-toggle='tab', ng-click='fetchGuilds()') Guilds +script(type='text/ng-template', id='partials/options.groups.tavern.html') + include ./tavern -.tab-content(ng-controller='PartyCtrl') - #groups-party.tab-pane.active - div(ng-show='group._id') +script(type='text/ng-template', id='partials/options.groups.party.html') + div(ng-show='group._id') + include ./group + div(ng-hide='group._id') + div(ng-show='user.invitations.party') + // #with required for the accept/reject buttons + // {#with _user.invitations.party as :party} + h2 You're Invited To {{user.invitations.party.name}} + a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)') Accept + a.btn.btn-danger(ng-click='reject()') Reject + // {/} + div(ng-hide='user.invitations.party', ng-controller='PartyCtrl') + h2 Create A Party + p + | You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter: + pre.prettyprint. + {{user.id}} + include ./create-group + +script(type='text/ng-template', id='partials/options.groups.guilds.html') + ul.nav.nav-tabs + li.active + a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds + li(ng-repeat='group in groups.guilds') + a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} + li + a(data-target='#groups-create-guild', data-toggle='tab') Create Guild + .tab-content + .tab-pane.active#groups-public-guilds + div(ng-repeat='invitation in user.invitations.guilds') + h3 You're Invited To {{invitation.name}} + a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept + a.btn.btn-danger(ng-click='reject(invitation)') Reject + // Public Groups + .options-group.option-large.whatever-options + input.option-content(type='text',ng-model='guildSearch', placeholder='Search') + table.table.table-striped + tr(ng-repeat='group in groups.public | filter:guildSearch') + td + ul.pull-right.challenge-accordion-header-specs + li {{group.members.length}} member(s) + li + // join / leave + a.btn.btn-small.btn-danger(ng-show='isMember(user, group)', ng-click='leave(group)') + i.icon-ban-circle + | Leave + a.btn.btn-small.btn-success(ng-hide='isMember(user, group)', ng-click='join(group)') + i.icon-ok + | Join + h4 {{group.name}} + p {{group.description}} + .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') include ./group - div(ng-hide='group._id') - div(ng-show='user.invitations.party') - // #with required for the accept/reject buttons - // {#with _user.invitations.party as :party} - h2 You're Invited To {{user.invitations.party.name}} - a.btn.btn-success(data-type='party', ng-click='join(user.invitations.party)') Accept - a.btn.btn-danger(ng-click='reject()') Reject - // {/} - div(ng-hide='user.invitations.party', ng-controller='PartyCtrl') - h2 Create A Party - p - | You are not in a party. You can either create one and invite friends, or if you want to join an existing party, have them enter: - pre.prettyprint. - {{user.id}} - include ./create-group - #groups-guilds.tab-pane(ng-controller='GuildsCtrl') - ul.nav.nav-tabs - li.active - a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds - li(ng-repeat='group in groups.guilds') - a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} - li - a(data-target='#groups-create-guild', data-toggle='tab') Create Guild - .tab-content - .tab-pane.active#groups-public-guilds - div(ng-repeat='invitation in user.invitations.guilds') - h3 You're Invited To {{invitation.name}} - a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept - a.btn.btn-danger(ng-click='reject(invitation)') Reject - // Public Groups - .options-group.option-large.whatever-options - input.option-content(type='text',ng-model='guildSearch', placeholder='Search') - table.table.table-striped - tr(ng-repeat='group in groups.public | filter:guildSearch') - td - ul.pull-right.challenge-accordion-header-specs - li {{group.members.length}} member(s) - li - // join / leave - a.btn.btn-small.btn-danger(ng-show='isMember(user, group)', ng-click='leave(group)') - i.icon-ban-circle - | Leave - a.btn.btn-small.btn-success(ng-hide='isMember(user, group)', ng-click='join(group)') - i.icon-ok - | Join - h4 {{group.name}} - p {{group.description}} - .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') - include ./group + .tab-pane#groups-create-guild + include ./create-group - .tab-pane#groups-create-guild - include ./create-group \ No newline at end of file +script(type='text/ng-template', id='partials/options.groups.html') + ul.nav.nav-tabs + li(ng-class="{ active: $state.includes('options.groups.tavern') }") + a(ui-sref='options.groups.tavern') + i.icon-eye-close + | Tavern + li(ng-class="{ active: $state.includes('options.groups.party') }") + a(ui-sref='options.groups.party') + | Party + li(ng-class="{ active: $state.includes('options.groups.guilds') }") + a(ui-sref='options.groups.guilds') + | Guilds + + .tab-content + .tab-pane.active + div(ui-view) \ No newline at end of file diff --git a/views/options/groups/tavern.jade b/views/options/groups/tavern.jade index a1f05d5533..54e21ce681 100644 --- a/views/options/groups/tavern.jade +++ b/views/options/groups/tavern.jade @@ -1,4 +1,4 @@ -.row-fluid(ng-controller='TavernCtrl') +.row-fluid .span4 .tavern-pane table diff --git a/views/options/index.jade b/views/options/index.jade index 75d917912f..e17a157089 100644 --- a/views/options/index.jade +++ b/views/options/index.jade @@ -1,64 +1,37 @@ -script(id='templates/habitrpg-options.html', type="text/ng-template") +include ./profile +include ./groups/index +include ./challenges +include ./inventory/index +include ./settings + +script(id='partials/options.html', type="text/ng-template") .grid .module.full-width span.option-box.pull-right.wallet include ../shared/gems ul.nav.nav-tabs - li.active - a(data-toggle='tab', data-target='#profile-tab') + li(ng-class="{ active: $state.includes('options.profile') }") + a(ui-sref='options.profile') i.icon-user | Profile - li - a(data-toggle='tab',data-target='#groups-tab', ng-click='fetchParty()') + li(ng-class="{ active: $state.includes('options.groups') }") + a(ui-sref='options.groups') i.icon-heart | Groups - li(ng-show='user.flags.dropsEnabled') - a(data-toggle='tab',data-target='#inventory-tab') + li(ng-class="{ active: $state.includes('options.inventory') }", ng-if='user.flags.dropsEnabled') + a(ui-sref='options.inventory') i.icon-gift | Inventory - li(ng-show='user.flags.dropsEnabled') - a(data-toggle='tab',data-target='#stable-tab') - i.icon-leaf - | Stable - li - a(data-toggle='tab',data-target='#tavern-tab', ng-click='fetchTavern()') - i.icon-eye-close - | Tavern - li - a(data-toggle='tab',data-target='#achievements-tab') - i.icon-certificate - | Achievements - li - a(data-toggle='tab',data-target='#challenges-tab') + li(ng-class="{ active: $state.includes('options.challenges') }") + a(ui-sref='options.challenges') i.icon-bullhorn | Challenges - li - a(data-toggle='tab',data-target='#settings-tab') + li(ng-class="{ active: $state.includes('options.settings') }") + a(ui-sref='options.settings') i.icon-wrench | Settings .tab-content - .tab-pane.active#profile-tab - include ./profile - - .tab-pane#groups-tab - include ./groups/index - - .tab-pane#inventory-tab - include ./inventory - - .tab-pane#stable-tab - include ./pets - - .tab-pane#tavern-tab - include ./groups/tavern - - .tab-pane#achievements-tab(ng-controller='UserCtrl') - include ../shared/profiles/achievements - - .tab-pane#challenges-tab - include ./challenges - - .tab-pane#settings-tab - include ./settings \ No newline at end of file + .tab-pane.active + div(ui-view) diff --git a/views/options/inventory.jade b/views/options/inventory.jade deleted file mode 100644 index 6fac392ac7..0000000000 --- a/views/options/inventory.jade +++ /dev/null @@ -1,54 +0,0 @@ -.row-fluid(ng-controller='InventoryCtrl') - .span6.border-right - h2 Inventory - p.well Combine eggs with hatching potions to make pets by clicking an egg and then a potion. Sell unwanted drops to Alexander the Merchant. - menu.inventory-list(type='list') - li.customize-menu - menu.pets-menu(label='Eggs ({{userEggs.length}})') - p(ng-show='userEggs.length < 1') You don't have any eggs yet. - div(ng-repeat='egg in userEggs track by $index') - button.customize-option(tooltip='{{egg.text}}', ng-click='chooseEgg(egg, $index)', class='Pet_Egg_{{egg.name}}', ng-class='selectableInventory(egg, selectedPotion.name, $index)') - p {{egg.text}} - li.customize-menu - menu.hatchingPotion-menu(label='Hatching Potions ({{userHatchingPotions.length}})') - p(ng-show='userHatchingPotions.length < 1') You don't have any hatching potions yet. - div(ng-repeat='hatchingPotion in userHatchingPotions track by $index') - button.customize-option(tooltip='{{hatchingPotion}}', ng-click='choosePotion(hatchingPotion, $index)', class='Pet_HatchingPotion_{{hatchingPotion}}', ng-class='selectableInventory(selectedEgg, hatchingPotion, $index)') - p {{hatchingPotion}} - - .span6 - h2 Market - .row-fluid(ng-controller='MarketCtrl') - table.NPC-Alex-container - tr - td - .NPC-Alex.pull-left - td - .popover.static-popover.fade.right.in - .arrow - h3.popover-title - a(target='_blank', href='http://www.kickstarter.com/profile/523661924') Alexander the Merchant - .popover-content - p - | Welcome to the Market. Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here! If you have unwanted drops, sell them to me. - p - button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') - | Sell {{selectedEgg.name}} for {{selectedEgg.value}} GP - button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') - | Sell {{selectedPotion.name}} for {{selectedPotion.value}} GP - - menu.inventory-list(type='list') - li.customize-menu - menu.pets-menu(label='Eggs') - div(ng-repeat='egg in eggs track by $index') - button.customize-option(tooltip='{{egg.text}} - {{egg.value}} Gem(s)', ng-click='buy("egg", egg)', class='Pet_Egg_{{egg.name}}') - p {{egg.text}} - - li.customize-menu - menu.pets-menu(label='Hatching Potions') - div(ng-repeat='hatchingPotion in hatchingPotions track by $index') - button.customize-option(tooltip='{{hatchingPotion.text}} - {{hatchingPotion.value}} Gem(s)', ng-click='buy("hatchingPotion", hatchingPotion)', class='Pet_HatchingPotion_{{hatchingPotion.name}}') - p {{hatchingPotion.text}} - - - diff --git a/views/options/inventory/index.jade b/views/options/inventory/index.jade new file mode 100644 index 0000000000..ff6a64a84d --- /dev/null +++ b/views/options/inventory/index.jade @@ -0,0 +1,15 @@ +include ./inventory +include ./stable + +script(type='text/ng-template', id='partials/options.inventory.html') + ul.nav.nav-tabs + li(ng-class="{ active: $state.includes('options.inventory.inventory') }") + a(ui-sref='options.inventory.inventory') + | Inventory + li(ng-class="{ active: $state.includes('options.inventory.stable') }") + a(ui-sref='options.inventory.stable') + | Stable + + .tab-content + .tab-pane.active + div(ui-view) \ No newline at end of file diff --git a/views/options/inventory/inventory.jade b/views/options/inventory/inventory.jade new file mode 100644 index 0000000000..98c71ea6e7 --- /dev/null +++ b/views/options/inventory/inventory.jade @@ -0,0 +1,55 @@ +script(type='text/ng-template', id='partials/options.inventory.inventory.html') + .row-fluid(ng-controller='InventoryCtrl') + .span6.border-right + h2 Inventory + p.well Combine eggs with hatching potions to make pets by clicking an egg and then a potion. Sell unwanted drops to Alexander the Merchant. + menu.inventory-list(type='list') + li.customize-menu + menu.pets-menu(label='Eggs ({{userEggs.length}})') + p(ng-show='userEggs.length < 1') You don't have any eggs yet. + div(ng-repeat='egg in userEggs track by $index') + button.customize-option(tooltip='{{egg.text}}', ng-click='chooseEgg(egg, $index)', class='Pet_Egg_{{egg.name}}', ng-class='selectableInventory(egg, selectedPotion.name, $index)') + p {{egg.text}} + li.customize-menu + menu.hatchingPotion-menu(label='Hatching Potions ({{userHatchingPotions.length}})') + p(ng-show='userHatchingPotions.length < 1') You don't have any hatching potions yet. + div(ng-repeat='hatchingPotion in userHatchingPotions track by $index') + button.customize-option(tooltip='{{hatchingPotion}}', ng-click='choosePotion(hatchingPotion, $index)', class='Pet_HatchingPotion_{{hatchingPotion}}', ng-class='selectableInventory(selectedEgg, hatchingPotion, $index)') + p {{hatchingPotion}} + + .span6 + h2 Market + .row-fluid(ng-controller='MarketCtrl') + table.NPC-Alex-container + tr + td + .NPC-Alex.pull-left + td + .popover.static-popover.fade.right.in + .arrow + h3.popover-title + a(target='_blank', href='http://www.kickstarter.com/profile/523661924') Alexander the Merchant + .popover-content + p + | Welcome to the Market. Dying to get that particular pet you're after, but don't want to wait for it to drop? Buy it here! If you have unwanted drops, sell them to me. + p + button.btn.btn-primary(ng-show='selectedEgg', ng-click='sellInventory()') + | Sell {{selectedEgg.name}} for {{selectedEgg.value}} GP + button.btn.btn-primary(ng-show='selectedPotion', ng-click='sellInventory()') + | Sell {{selectedPotion.name}} for {{selectedPotion.value}} GP + + menu.inventory-list(type='list') + li.customize-menu + menu.pets-menu(label='Eggs') + div(ng-repeat='egg in eggs track by $index') + button.customize-option(tooltip='{{egg.text}} - {{egg.value}} Gem(s)', ng-click='buy("egg", egg)', class='Pet_Egg_{{egg.name}}') + p {{egg.text}} + + li.customize-menu + menu.pets-menu(label='Hatching Potions') + div(ng-repeat='hatchingPotion in hatchingPotions track by $index') + button.customize-option(tooltip='{{hatchingPotion.text}} - {{hatchingPotion.value}} Gem(s)', ng-click='buy("hatchingPotion", hatchingPotion)', class='Pet_HatchingPotion_{{hatchingPotion.name}}') + p {{hatchingPotion.text}} + + + diff --git a/views/options/inventory/stable.jade b/views/options/inventory/stable.jade new file mode 100644 index 0000000000..e4cf11084e --- /dev/null +++ b/views/options/inventory/stable.jade @@ -0,0 +1,30 @@ +script(type='text/ng-template', id='partials/options.inventory.stable.html') + .stable(ng-controller='PetsCtrl') + .NPC-Matt + .popover.static-popover.fade.right.in(style='max-width: 550px; margin-left: 10px;') + .arrow + h3.popover-title + a(target='_blank', href='http://www.kickstarter.com/profile/mattboch') Matt Boch + .popover-content + p + | Welcome to the Stable! I'm Matt, the beast master. Choose a pet here to adventure at your side - they aren't much help yet, but I forsee a time when they're able to + a(href='https://trello.com/card/mounts/50e5d3684fe3a7266b0036d6/221') grow into powerful steeds + | ! Until that day, + a(target='_blank', href='https://f.cloud.github.com/assets/2374703/164631/3ed5fa6c-78cd-11e2-8743-f65ac477b55e.png') have a look-see + | at all the pets you can collect. + h4 {{userPets.length}} / {{totalPets}} Pets Found + + menu.pets(type='list') + li.customize-menu(ng-repeat='pet in pets') + menu + div(ng-repeat='potion in hatchingPotions', tooltip='{{potion.name}} {{pet.name}}') + button(class="pet-button Pet-{{pet.name}}-{{potion.name}}", ng-show='hasPet(pet.name, potion.name)', ng-class="{active: isCurrentPet(pet.name, potion.name)}", ng-click='choosePet(pet.name, potion.name)') + button(class="pet-button pet-not-owned", ng-hide='hasPet(pet.name, potion.name)') + img(src='/bower_components/habitrpg-shared/img/PixelPaw.png') + + h4 Rare Pets + menu + div(ng-if='hasPet("Wolf", "Veteran")') + button(class="pet-button Pet-Wolf-Veteran", ng-class='{active: isCurrentPet("Wolf", "Veteran")}', ng-click='choosePet("Wolf", "Veteran")', tooltip='Veteran Wolf') + div(ng-if='hasPet("Wolf", "Cerberus")') + button(class="pet-button Pet-Wolf-Cerberus", ng-class='{active: isCurrentPet("Wolf", "Cerberus")}', ng-click='choosePet("Wolf", "Cerberus")', tooltip='Cerberus Pup') \ No newline at end of file diff --git a/views/options/pets.jade b/views/options/pets.jade deleted file mode 100644 index 80d8b1a899..0000000000 --- a/views/options/pets.jade +++ /dev/null @@ -1,29 +0,0 @@ -.stable(ng-controller='PetsCtrl') - .NPC-Matt - .popover.static-popover.fade.right.in(style='max-width: 550px; margin-left: 10px;') - .arrow - h3.popover-title - a(target='_blank', href='http://www.kickstarter.com/profile/mattboch') Matt Boch - .popover-content - p - | Welcome to the Stable! I'm Matt, the beast master. Choose a pet here to adventure at your side - they aren't much help yet, but I forsee a time when they're able to - a(href='https://trello.com/card/mounts/50e5d3684fe3a7266b0036d6/221') grow into powerful steeds - | ! Until that day, - a(target='_blank', href='https://f.cloud.github.com/assets/2374703/164631/3ed5fa6c-78cd-11e2-8743-f65ac477b55e.png') have a look-see - | at all the pets you can collect. - h4 {{userPets.length}} / {{totalPets}} Pets Found - - menu.pets(type='list') - li.customize-menu(ng-repeat='pet in pets') - menu - div(ng-repeat='potion in hatchingPotions', tooltip='{{potion.name}} {{pet.name}}') - button(class="pet-button Pet-{{pet.name}}-{{potion.name}}", ng-show='hasPet(pet.name, potion.name)', ng-class="{active: isCurrentPet(pet.name, potion.name)}", ng-click='choosePet(pet.name, potion.name)') - button(class="pet-button pet-not-owned", ng-hide='hasPet(pet.name, potion.name)') - img(src='/bower_components/habitrpg-shared/img/PixelPaw.png') - - h4 Rare Pets - menu - div(ng-if='hasPet("Wolf", "Veteran")') - button(class="pet-button Pet-Wolf-Veteran", ng-class='{active: isCurrentPet("Wolf", "Veteran")}', ng-click='choosePet("Wolf", "Veteran")', tooltip='Veteran Wolf') - div(ng-if='hasPet("Wolf", "Cerberus")') - button(class="pet-button Pet-Wolf-Cerberus", ng-class='{active: isCurrentPet("Wolf", "Cerberus")}', ng-click='choosePet("Wolf", "Cerberus")', tooltip='Cerberus Pup') \ No newline at end of file diff --git a/views/options/profile.jade b/views/options/profile.jade index 98e1ebebd3..8ee97c5fdb 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -1,128 +1,145 @@ -.row-fluid(ng-controller='UserCtrl') - - // ---- Customize ----- - .span4.border-right - menu(type='list') - // gender - li.customize-menu - menu(label='Head') - menu - button.m_head_0.customize-option(type='button', ng-click='set("preferences.gender","m")') - button.f_head_0.customize-option(type='button', ng-click='set("preferences.gender","f")') - label.checkbox - input(type='checkbox', ng-model='user.preferences.showHelm', ng-change='toggleHelm(user.preferences.showHelm)') - | Show Helm - hr - - // hair - li.customize-menu - menu(label='Hair') - button(class='{{user.preferences.gender}}_hair_blond customize-option', type='button', ng-click='set("preferences.hair","blond")') - button(class='{{user.preferences.gender}}_hair_black customize-option', type='button', ng-click='set("preferences.hair","black")') - button(class='{{user.preferences.gender}}_hair_brown customize-option', type='button', ng-click='set("preferences.hair","brown")') - button(class='{{user.preferences.gender}}_hair_white customize-option', type='button', ng-click='set("preferences.hair","white")') - button(class='{{user.preferences.gender}}_hair_red customize-option', type='button', ng-click='set("preferences.hair","red")') - hr - - // skin - li.customize-menu - menu(label='Basic Skins') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_asian', ng-click='set("preferences.skin","asian")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_white', ng-click='set("preferences.skin","white")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ea8349', ng-click='set("preferences.skin","ea8349")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_c06534', ng-click='set("preferences.skin","c06534")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_98461a', ng-click='set("preferences.skin","98461a")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_black', ng-click='set("preferences.skin","black")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_dead', ng-click='set("preferences.skin","dead")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_orc', ng-click='set("preferences.skin","orc")') - - // Rainbow Skin - h5. - Rainbow Skins - 2/skin - //menu(label='Rainbow Skins (2G / skin)') - menu - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_eb052b', ng-class='{locked: !user.purchased.skin.eb052b}', ng-click='unlock("skin.eb052b")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f69922', ng-class='{locked: !user.purchased.skin.f69922}', ng-click='unlock("skin.f69922")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f5d70f', ng-class='{locked: !user.purchased.skin.f5d70f}', ng-click='unlock("skin.f5d70f")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_0ff591', ng-class='{locked: !user.purchased.skin.0ff591}', ng-click='unlock("skin.0ff591")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_2b43f6', ng-class='{locked: !user.purchased.skin.2b43f6}', ng-click='unlock("skin.2b43f6")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_d7a9f7', ng-class='{locked: !user.purchased.skin.d7a9f7}', ng-click='unlock("skin.d7a9f7")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_800ed0', ng-class='{locked: !user.purchased.skin.800ed0}', ng-click='unlock("skin.800ed0")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_rainbow', ng-class='{locked: !user.purchased.skin.rainbow}', ng-click='unlock("skin.rainbow")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock(["skin.eb052b", "skin.f69922", "skin.f5d70f", "skin.0ff591", "skin.2b43f6", "skin.d7a9f7", "skin.800ed0", "skin.rainbow"])') Unlock Set - 5 - - // Special Events - div.well.limited-edition - .label.label-info.pull-right(popover='Available for purchase until November 10th (but permanently in your options if purchased).', popover-title='Limited Edition', popover-placement='right', popover-trigger='mouseenter') - | Limited Edition  - i.icon.icon-question-sign - h5. - Spooky Skins - 2/skin - //menu(label='Spooky Skins (2 Gems / skin)') - menu - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_monster', ng-class='{locked: !user.purchased.skin.monster}', ng-click='unlock("skin.monster")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_pumpkin', ng-class='{locked: !user.purchased.skin.pumpkin}', ng-click='unlock("skin.pumpkin")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_skeleton', ng-class='{locked: !user.purchased.skin.skeleton}', ng-click='unlock("skin.skeleton")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_zombie', ng-class='{locked: !user.purchased.skin.zombie}', ng-click='unlock("skin.zombie")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ghost', ng-class='{locked: !user.purchased.skin.ghost}', ng-click='unlock("skin.ghost")') - button.customize-option(type='button', class='{{user.preferences.gender}}_skin_shadow', ng-class='{locked: !user.purchased.skin.shadow}', ng-click='unlock("skin.shadow")') - button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.monster && user.purchased.skin.pumpkin && user.purchased.skin.skeleton && user.purchased.skin.zombie && user.purchased.skin.ghost && user.purchased.skin.shadow', ng-click='unlock(["skin.monster", "skin.pumpkin", "skin.skeleton", "skin.zombie", "skin.ghost", "skin.shadow"])') Unlock Set - 5 - - - menu(ng-show='user.preferences.gender=="f"', type='list') +script(id='partials/options.profile.avatar.html', type='text/ng-template') + .row-fluid + .span4.border-right + menu(type='list') + // body-type li.customize-menu - menu(label='Clothing') - button.f_armor_0_v1.customize-option(type='button', ng-click='set("preferences.armorSet","v1")') - button.f_armor_0_v2.customize-option(type='button', ng-click='set("preferences.armorSet","v2")') + menu(label='Head') + menu + button.m_head_0.customize-option(type='button', ng-click='set("preferences.gender","m")') + button.f_head_0.customize-option(type='button', ng-click='set("preferences.gender","f")') + label.checkbox + input(type='checkbox', ng-model='user.preferences.showHelm', ng-change='toggleHelm(user.preferences.showHelm)') + | Show Helm - // ------ Stats ------ - .span4.border-right - include ../shared/profiles/stats + menu(ng-show='user.preferences.gender=="f"', type='list') + li.customize-menu + menu(label='Clothing') + button.f_armor_0_v1.customize-option(type='button', ng-click='set("preferences.armorSet","v1")') + button.f_armor_0_v2.customize-option(type='button', ng-click='set("preferences.armorSet","v2")') - // ------- Edit ------- - .span4 - button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile') Edit - button.btn.btn-primary(ng-click='save()', ng-show='_editing.profile') Save - div(ng-show='!_editing.profile') - h4 Display Name - span(ng-show='profile.profile.name') {{profile.profile.name}} - span.muted(ng-hide='profile.profile.name') - None - + .span4.border-right + // hair + li.customize-menu + menu(label='Hair') + button(class='{{user.preferences.gender}}_hair_blond customize-option', type='button', ng-click='set("preferences.hair","blond")') + button(class='{{user.preferences.gender}}_hair_black customize-option', type='button', ng-click='set("preferences.hair","black")') + button(class='{{user.preferences.gender}}_hair_brown customize-option', type='button', ng-click='set("preferences.hair","brown")') + button(class='{{user.preferences.gender}}_hair_white customize-option', type='button', ng-click='set("preferences.hair","white")') + button(class='{{user.preferences.gender}}_hair_red customize-option', type='button', ng-click='set("preferences.hair","red")') - h4 Photo - img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') - span.muted(ng-hide='profile.profile.imageUrl') - None - + .span4.border-right + // skin + li.customize-menu + menu(label='Basic Skins') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_asian', ng-click='set("preferences.skin","asian")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_white', ng-click='set("preferences.skin","white")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ea8349', ng-click='set("preferences.skin","ea8349")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_c06534', ng-click='set("preferences.skin","c06534")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_98461a', ng-click='set("preferences.skin","98461a")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_black', ng-click='set("preferences.skin","black")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_dead', ng-click='set("preferences.skin","dead")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_orc', ng-click='set("preferences.skin","orc")') - h4 Blurb - markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb') - span.muted(ng-hide='profile.profile.blurb') - None - - //{{profile.profile.blurb | linky:'_blank'}} + // Rainbow Skin + h5. + Rainbow Skins - 2/skin + //menu(label='Rainbow Skins (2G / skin)') + menu + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_eb052b', ng-class='{locked: !user.purchased.skin.eb052b}', ng-click='unlock("skin.eb052b")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f69922', ng-class='{locked: !user.purchased.skin.f69922}', ng-click='unlock("skin.f69922")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_f5d70f', ng-class='{locked: !user.purchased.skin.f5d70f}', ng-click='unlock("skin.f5d70f")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_0ff591', ng-class='{locked: !user.purchased.skin.0ff591}', ng-click='unlock("skin.0ff591")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_2b43f6', ng-class='{locked: !user.purchased.skin.2b43f6}', ng-click='unlock("skin.2b43f6")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_d7a9f7', ng-class='{locked: !user.purchased.skin.d7a9f7}', ng-click='unlock("skin.d7a9f7")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_800ed0', ng-class='{locked: !user.purchased.skin.800ed0}', ng-click='unlock("skin.800ed0")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_rainbow', ng-class='{locked: !user.purchased.skin.rainbow}', ng-click='unlock("skin.rainbow")') + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.eb052b && user.purchased.skin.f69922 && user.purchased.skin.f5d70f && user.purchased.skin.0ff591 && user.purchased.skin.2b43f6 && user.purchased.skin.d7a9f7 && user.purchased.skin.800ed0 && user.purchased.skin.rainbow', ng-click='unlock(["skin.eb052b", "skin.f69922", "skin.f5d70f", "skin.0ff591", "skin.2b43f6", "skin.d7a9f7", "skin.800ed0", "skin.rainbow"])') Unlock Set - 5 - h4 Websites - ul(ng-show='profile.profile.websites.length > 0') - // TODO let's remove links eventually, since we can do markdown on profiles - li(ng-repeat='website in profile.profile.websites') - a(target='_blank', ng-href='{{website}}') {{website}} - span.muted(ng-hide='profile.profile.websites.length > 0') - None - + // Special Events + div.well.limited-edition + .label.label-info.pull-right(popover='Available for purchase until November 10th (but permanently in your options if purchased).', popover-title='Limited Edition', popover-placement='right', popover-trigger='mouseenter') + | Limited Edition  + i.icon.icon-question-sign + h5. + Spooky Skins - 2/skin + //menu(label='Spooky Skins (2 Gems / skin)') + menu + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_monster', ng-class='{locked: !user.purchased.skin.monster}', ng-click='unlock("skin.monster")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_pumpkin', ng-class='{locked: !user.purchased.skin.pumpkin}', ng-click='unlock("skin.pumpkin")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_skeleton', ng-class='{locked: !user.purchased.skin.skeleton}', ng-click='unlock("skin.skeleton")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_zombie', ng-class='{locked: !user.purchased.skin.zombie}', ng-click='unlock("skin.zombie")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_ghost', ng-class='{locked: !user.purchased.skin.ghost}', ng-click='unlock("skin.ghost")') + button.customize-option(type='button', class='{{user.preferences.gender}}_skin_shadow', ng-class='{locked: !user.purchased.skin.shadow}', ng-click='unlock("skin.shadow")') + button.btn.btn-small.btn-primary(ng-hide='user.purchased.skin.monster && user.purchased.skin.pumpkin && user.purchased.skin.skeleton && user.purchased.skin.zombie && user.purchased.skin.ghost && user.purchased.skin.shadow', ng-click='unlock(["skin.monster", "skin.pumpkin", "skin.skeleton", "skin.zombie", "skin.ghost", "skin.shadow"])') Unlock Set - 5 - div.whatever-options(ng-show='_editing.profile') - // TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak - .control-group.option-large - label.control-label Display Name - input.option-content(type='text', placeholder='Full Name', ng-model='editingProfile.name') - .control-group.option-large - label.control-label Photo Url - input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder='Image Url') - .control-group.option-large - label.control-label Blurb - textarea.option-content(style='height:15em;', placeholder='Blurb', ng-model='editingProfile.blurb') - include ../shared/formatting-help - .control-group.option-large - label.control-label Websites - form(ng-submit='addWebsite()') - input.option-content(type='url', ng-model='_newWebsite', placeholder='Add Website') - ul - // would prefer if there were and index in #each, instead using data-website to search with indexOf - li(ng-repeat='website in editingProfile.websites') - | {{website}} - a(ng-click='removeWebsite($index)') - i.icon-remove +script(id='partials/options.profile.stats.html', type='text/ng-template') + .row-fluid + .span6.border-right + include ../shared/profiles/stats + .span6.border-right + include ../shared/profiles/achievements + +script(id='partials/options.profile.profile.html', type='text/ng-template') + button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile') Edit + button.btn.btn-primary(ng-click='save()', ng-show='_editing.profile') Save + div(ng-show='!_editing.profile') + h4 Display Name + span(ng-show='profile.profile.name') {{profile.profile.name}} + span.muted(ng-hide='profile.profile.name') - None - + + h4 Photo + img(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}') + span.muted(ng-hide='profile.profile.imageUrl') - None - + + h4 Blurb + markdown(ng-show='profile.profile.blurb', ng-model='profile.profile.blurb') + span.muted(ng-hide='profile.profile.blurb') - None - + //{{profile.profile.blurb | linky:'_blank'}} + + h4 Websites + ul(ng-show='profile.profile.websites.length > 0') + // TODO let's remove links eventually, since we can do markdown on profiles + li(ng-repeat='website in profile.profile.websites') + a(target='_blank', ng-href='{{website}}') {{website}} + span.muted(ng-hide='profile.profile.websites.length > 0') - None - + + div.whatever-options(ng-show='_editing.profile') + // TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak + .control-group.option-large + label.control-label Display Name + input.option-content(type='text', placeholder='Full Name', ng-model='editingProfile.name') + .control-group.option-large + label.control-label Photo Url + input.option-content(type='url', ng-model='editingProfile.imageUrl', placeholder='Image Url') + .control-group.option-large + label.control-label Blurb + textarea.option-content(style='height:15em;', placeholder='Blurb', ng-model='editingProfile.blurb') + br + include ../shared/formatting-help + .control-group.option-large + label.control-label Websites + form(ng-submit='addWebsite()') + input.option-content(type='url', ng-model='_newWebsite', placeholder='Add Website') + ul + // would prefer if there were and index in #each, instead using data-website to search with indexOf + li(ng-repeat='website in editingProfile.websites') + | {{website}} + a(ng-click='removeWebsite($index)') + i.icon-remove + +script(id='partials/options.profile.html', type="text/ng-template") + ul.nav.nav-tabs + li(ng-class="{ active: $state.includes('options.profile.avatar') }") + a(ui-sref='options.profile.avatar') + | Avatar + li(ng-class="{ active: $state.includes('options.profile.stats') }") + a(ui-sref='options.profile.stats') + | Stats & Achievements + li(ng-class="{ active: $state.includes('options.profile.profile') }") + a(ui-sref='options.profile.profile') + | Profile + + .tab-content + .tab-pane.active + div(ui-view) diff --git a/views/options/settings.jade b/views/options/settings.jade index a3f8201d8c..4c957c119b 100644 --- a/views/options/settings.jade +++ b/views/options/settings.jade @@ -1,43 +1,44 @@ -.row-fluid(ng-controller='SettingsCtrl') - .personal-options.span6.border-right - h2 Settings - h4 Custom Day Start - .option-group.option-short - input.option-content.option-time(type='number', min='0', max='24', ng-model='user.preferences.dayStart', ng-change='saveDayStart()') - span.input-suffix :00 (24h clock) - div - small - | Habit defaults to check and reset your dailies at midnight in your time zone each day. You can customize the hour here (enter a number between 0 and 24). - hr - h4 Misc - button.btn(ng-hide='user.preferences.hideHeader', ng-click='set("preferences.hideHeader",true)') Hide Header - button.btn(ng-show='user.preferences.hideHeader', ng-click='set("preferences.hideHeader",false)') Show Header - button.btn(ng-click='showTour()') Show Tour - button.btn(ng-click='showBailey()') Show Bailey - - div(ng-show='user.auth.local') +script(type='text/ng-template', id='partials/options.settings.html') + .row-fluid + .personal-options.span6.border-right + h2 Settings + h4 Custom Day Start + .option-group.option-short + input.option-content.option-time(type='number', min='0', max='24', ng-model='user.preferences.dayStart', ng-change='saveDayStart()') + span.input-suffix :00 (24h clock) + div + small + | Habit defaults to check and reset your dailies at midnight in your time zone each day. You can customize the hour here (enter a number between 0 and 24). hr - h4 Change Password - form(ng-submit='changePassword(changePass)', ng-show='user.auth.local') - .control-group - input(type='password', placeholder='Old Password', ng-model='changePass.oldPassword', required) - .control-group - input(type='password', placeholder='New Password', ng-model='changePass.newPassword', required) - .control-group - input(type='password', placeholder='Confirm New Password', ng-model='changePass.confirmNewPassword', required) - input.btn(type='submit', value='Submit') + h4 Misc + button.btn(ng-hide='user.preferences.hideHeader', ng-click='set("preferences.hideHeader",true)') Hide Header + button.btn(ng-show='user.preferences.hideHeader', ng-click='set("preferences.hideHeader",false)') Show Header + button.btn(ng-click='showTour()') Show Tour + button.btn(ng-click='showBailey()') Show Bailey - hr - h4 Danger Zone - a.btn.btn-danger(ng-click='modals.reset = true', tooltip='Resets your entire account (dangerous).') Reset - a.btn.btn-danger(ng-click='modals.restore = true', tooltip='Restores attributes to your character.') Restore - a.btn.btn-danger(ng-click='modals.delete = true', tooltip='Delete your account.') Delete - .span6 - h2 API - small Copy these for use in third party applications. - h6 User ID - pre.prettyprint {{user.id}} - h6 API Token - pre.prettyprint {{user.apiToken}} - h6 QR Code - img(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7Baddress%3A%22https%3A%2F%2Fhabitrpg.com%22%2Cuser%3A%22{{user.id}}%22%2Ckey%3A%22{{user.apiToken}}%22%7D,&choe=UTF-8&chld=L', alt='qrcode') + div(ng-show='user.auth.local') + hr + h4 Change Password + form(ng-submit='changePassword(changePass)', ng-show='user.auth.local') + .control-group + input(type='password', placeholder='Old Password', ng-model='changePass.oldPassword', required) + .control-group + input(type='password', placeholder='New Password', ng-model='changePass.newPassword', required) + .control-group + input(type='password', placeholder='Confirm New Password', ng-model='changePass.confirmNewPassword', required) + input.btn(type='submit', value='Submit') + + hr + h4 Danger Zone + a.btn.btn-danger(ng-click='modals.reset = true', tooltip='Resets your entire account (dangerous).') Reset + a.btn.btn-danger(ng-click='modals.restore = true', tooltip='Restores attributes to your character.') Restore + a.btn.btn-danger(ng-click='modals.delete = true', tooltip='Delete your account.') Delete + .span6 + h2 API + small Copy these for use in third party applications. + h6 User ID + pre.prettyprint {{user.id}} + h6 API Token + pre.prettyprint {{user.apiToken}} + h6 QR Code + img(src='https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=%7Baddress%3A%22https%3A%2F%2Fhabitrpg.com%22%2Cuser%3A%22{{user.id}}%22%2Ckey%3A%22{{user.apiToken}}%22%7D,&choe=UTF-8&chld=L', alt='qrcode') From 4ac0ef4ac2551f9d583680b5d89fb03299251a9f Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 28 Oct 2013 12:03:20 -0700 Subject: [PATCH 11/15] challenges: migration script for TaskSchema subdocs, cleanup tags, remove old challenges --- migrations/20131028_cleanup_deleted_tags.js | 17 -------- .../20131028_task_subdocs_and_tags_cleanup.js | 40 +++++++++++++++++++ src/models/task.js | 2 +- src/models/user.js | 24 ----------- 4 files changed, 41 insertions(+), 42 deletions(-) delete mode 100644 migrations/20131028_cleanup_deleted_tags.js create mode 100644 migrations/20131028_task_subdocs_and_tags_cleanup.js diff --git a/migrations/20131028_cleanup_deleted_tags.js b/migrations/20131028_cleanup_deleted_tags.js deleted file mode 100644 index 165088dc6a..0000000000 --- a/migrations/20131028_cleanup_deleted_tags.js +++ /dev/null @@ -1,17 +0,0 @@ -_.each(db.users.find(), function(user){ - var tags = user.tags; - - _.each(user.tasks, function(task){ - _.each(task.tags, function(val, key){ - _.each(tags, function(tag){ - if(key == tag.id) delete task.tags[key]; - }); - }; - }); - - try { - db.users.update({_id:user._id}, user); - } catch(e) { - print(e); - } -}); diff --git a/migrations/20131028_task_subdocs_and_tags_cleanup.js b/migrations/20131028_task_subdocs_and_tags_cleanup.js new file mode 100644 index 0000000000..f01cf967da --- /dev/null +++ b/migrations/20131028_task_subdocs_and_tags_cleanup.js @@ -0,0 +1,40 @@ +db.users.find().forEach(function(user){ + + // Cleanup broken tags + _.each(user.tasks, function(task){ + _.each(task.tags, function(val, key){ + _.each(user.tags, function(tag){ + if(key == tag.id) delete task.tags[key]; + }); + }); + }); + + // Migrate to TaskSchema subdocs!! + if (!user.tasks) { + printjson(user.auth); + // FIXME before deploying! + } else { + _.each(['habit', 'daily', 'todo', 'reward'], function(type) { + // we use _.transform instead of a simple _.where in order to maintain sort-order + user[type + "s"] = _.reduce(user[type + "Ids"], function(m, tid) { + var task = user.tasks[tid]; + if (!task) return m; // remove null tasks + //if (!user.tasks[tid].tags) user.tasks[tid].tags = {}; // shouldn't be necessary, since TaskSchema.tags has default {} + task._id = task.id; + m.push(task); + return m; + }, []); + delete user[type + 'Ids']; + }); + delete user.tasks; + } + + try { + db.users.update({_id:user._id}, user); + } catch(e) { + print(e); + } +}); + +// Remove old groups.*.challenges, they're not compatible with the new system +db.groups.update({},{$pull:{challenges:1}},{multi:true}); diff --git a/src/models/task.js b/src/models/task.js index fd89af35a4..3935e0f232 100644 --- a/src/models/task.js +++ b/src/models/task.js @@ -17,7 +17,7 @@ var TaskSchema = new Schema({ _id:{type: String,'default': helpers.uuid}, text: String, notes: {type: String, 'default': ''}, - tags: Schema.Types.Mixed, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, + tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true }, type: {type:String, 'default': 'habit'}, // habit, daily up: {type: Boolean, 'default': true}, down: {type: Boolean, 'default': true}, diff --git a/src/models/user.js b/src/models/user.js index 6462af039e..4f1438895b 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -64,10 +64,6 @@ var UserSchema = new Schema({ }, balance: Number, - habitIds: Array, - dailyIds: Array, - todoIds: Array, - rewardIds: Array, filters: {type: Schema.Types.Mixed, 'default': {}}, purchased: { @@ -217,26 +213,6 @@ var UserSchema = new Schema({ minimize: false // So empty objects are returned }); -// Legacy Derby Function? -// ---------------------- -// Derby requires a strange storage format for somethign called "refLists". Here we hook into loading the data, so we -// can provide a more "expected" storage format for our various helper methods. Since the attributes are passed by reference, -// the underlying data will be modified too - so when we save back to the database, it saves it in the way Derby likes. -// This will go away after the rewrite is complete - -//FIXME use this in migration -/*function transformTaskLists(doc) { - _.each(['habit', 'daily', 'todo', 'reward'], function(type) { - // we use _.transform instead of a simple _.where in order to maintain sort-order - doc[type + "s"] = _.reduce(doc[type + "Ids"], function(m, tid) { - if (!doc.tasks[tid]) return m; // FIXME tmp hotfix, people still have null tasks? - if (!doc.tasks[tid].tags) doc.tasks[tid].tags = {}; // FIXME remove this when we switch tasks to subdocs and can define tags default in schema - m.push(doc.tasks[tid]); - return m; - }, []); - }); -}*/ - UserSchema.methods.toJSON = function() { var doc = this.toObject(); doc.id = doc._id; From c5671550f11db74367c14b72d6d981dca16a7d28 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 28 Oct 2013 17:31:27 -0700 Subject: [PATCH 12/15] challenges: add group.memberCount and ability to specifiy field projections in GET request /api/v1/groups?type=guild&fields=name,description,chat --- .../20131028_task_subdocs_and_tags_cleanup.js | 5 + package.json | 2 +- public/js/services/groupServices.js | 2 +- src/controllers/groups.js | 97 +++++++++++-------- src/models/group.js | 4 + views/options/profile.jade | 6 +- 6 files changed, 71 insertions(+), 45 deletions(-) diff --git a/migrations/20131028_task_subdocs_and_tags_cleanup.js b/migrations/20131028_task_subdocs_and_tags_cleanup.js index f01cf967da..469d45a088 100644 --- a/migrations/20131028_task_subdocs_and_tags_cleanup.js +++ b/migrations/20131028_task_subdocs_and_tags_cleanup.js @@ -38,3 +38,8 @@ db.users.find().forEach(function(user){ // Remove old groups.*.challenges, they're not compatible with the new system db.groups.update({},{$pull:{challenges:1}},{multi:true}); +db.groups.find().forEach(function(group){ + db.groups.update({_id:group._id}, { + $set:{memberCount: _.size(group.members)} + }) +}); diff --git a/package.json b/package.json index 07a05f0925..56ecf29414 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "lodash": "~1.3.1", "async": "~0.2.9", "optimist": "~0.5.2", - "mongoose": "~3.6.18", + "mongoose": "~3.6.20", "stylus": "~0.37.0", "grunt": "~0.4.1", "grunt-contrib-uglify": "~0.2.4", diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 47c1b610d8..8c6d9a4281 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -34,7 +34,7 @@ angular.module('groupServices', ['ngResource']). }; // But we don't defer triggering Party, since we always need it for the header if nothing else - Group.query({type:'party'}, function(_groups){ + Group.query({type:'party', fields:'members'}, function(_groups){ partyQ.resolve(_groups[0]); Members.populate(_groups[0]); }) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index cb36ffa7b0..3b563e047d 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -16,8 +16,8 @@ var api = module.exports; ------------------------------------------------------------------------ */ -var usernameFields = 'auth.local.username auth.facebook.displayName auth.facebook.givenName auth.facebook.familyName auth.facebook.name'; -var partyFields = 'profile preferences items stats achievements party backer flags.rest auth.timestamps ' + usernameFields; +var itemFields = 'items.armor items.head items.shield items.weapon items.currentPet'; +var partyFields = 'profile preferences stats achievements party backer flags.rest auth.timestamps ' + itemFields; function removeSelf(group, user){ group.members = _.filter(group.members, function(m){return m._id != user._id}); @@ -31,65 +31,83 @@ api.getMember = function(req, res) { }) } +function sanitizeMemberFields(fields, fallback) { + if (!fields) return fallback; + if (~fields.indexOf(',')) fields = fields.replace(/\,/g, ' '); + var intersection = _.intersection( (partyFields + ' items').split(' '), (fields).split(' ') ); + return _.isEmpty(_.filter(intersection, function(f){return f!=''})) ? fallback : intersection; +} + /** - * Get groups. If req.query.type privided, returned as an array (so ngResource can use). If not, returned as + * Get groups. If req.query.type provided, returned as an array (so ngResource can use). If not, returned as * object {guilds, public, party, tavern}. req.query.type can be comma-separated `type=guilds,party` - * @param req - * @param res - * @param next + * + * We can request specific fields to keep things slim. Not specified gives you full detail. Request them as: + * - simple: ?fields=title,description,chat,challenges,memberCount + * - sub-fields: ?fields=title,description,members&fields.members=profile.name */ api.getGroups = function(req, res, next) { var user = res.locals.user; - - // if ?minimal=true, just send down names - if (req.query.minimal) { - return Group.find({members: {'$in': [user._id]}}).select('name _id').exec(function(err, groups){ - if (err) return res.json(500, {err:err}); - res.json(groups); - }); - } - var type = req.query.type && req.query.type.split(','); + var fields = req.query.fields && req.query.fields.replace(/\,/g, ' ') + var challengeFields = 'name description'; // public challenge fields, request specific challenge for more details // First get all groups async.parallel({ + party: function(cb) { if (type && !~type.indexOf('party')) return cb(null, {}); - Group - .findOne({type: 'party', members: {'$in': [user._id]}}) - .populate({ - path: 'members', - //match: {_id: {$ne: user._id}}, //fixme this causes it to hang?? - select: partyFields - }) - .exec(cb); + var query = Group.findOne({type: 'party', members: {'$in': [user._id]}}); + if (fields) { + query.select(fields); + if (~fields.indexOf('members')) + query.populate('members', sanitizeMemberFields(req.query['fields.members'], partyFields)); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(function(err, group){ + if (err) return cb(err); + // Remove self from party (match in `populate()` hangs - mongoose bug) + removeSelf(group, user); + cb(null, group); + }); }, + guilds: function(cb) { - if (type && !~type.indexOf('guilds')) return cb(null, []); - Group.find({type: 'guild', members: {'$in': [user._id]}}).populate('members', usernameFields).exec(cb); -// Group.find({type: 'guild', members: {'$in': [user._id]}}, cb); + if (type && !~type.indexOf('guilds')) return cb(null, {}); + var query = Group.find({type: 'guild', members: {'$in': [user._id]}}); + if (fields) { + query.select(fields); + if (~fields.indexOf('members')) + query.populate('members', sanitizeMemberFields(req.query['fields.members'], 'profile.name')); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(cb); }, + tavern: function(cb) { if (type && !~type.indexOf('tavern')) return cb(null, {}); - Group.findOne({_id: 'habitrpg'}, cb); + var query = Group.findById('habitrpg'); + if (fields) { + query.select(fields); + if (~fields.indexOf('challenges')) + query.populate('challenges', challengeFields); + } + query.exec(cb); }, + "public": function(cb) { if (type && !~type.indexOf('public')) return cb(null, []); - Group.find({privacy: 'public'}, {name:1, description:1, members:1}, cb); + Group.find({privacy: 'public'}) + .select('name description memberCount') + .sort('-memberCount') + .exec(cb); } + }, function(err, results){ if (err) return res.json(500, {err: err}); - // Remove self from party (see above failing `match` directive in `populate` - if (results.party) { - removeSelf(results.party, user); - } - - // Sort public groups by members length (not easily doable in mongoose) - results.public = _.sortBy(results.public, function(group){ - return -group.members.length; - }); - // If they're requesting a specific type, let's return it as an array so that $ngResource // can utilize it properly if (type) { @@ -97,7 +115,6 @@ api.getGroups = function(req, res, next) { return m.concat(_.isArray(results[t]) ? results[t] : [results[t]]); }, []); } - res.json(results); }) }; @@ -157,7 +174,7 @@ api.updateGroup = function(req, res, next) { async.series([ function(cb){group.save(cb);}, function(cb){ - var fields = group.type == 'party' ? partyFields : usernameFields; + var fields = group.type == 'party' ? partyFields : 'profile.name'; Group.findById(group._id).populate('members', fields).exec(cb); } ], function(err, results){ diff --git a/src/models/group.js b/src/models/group.js index 6aed645bea..c297f31acc 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -26,6 +26,8 @@ var GroupSchema = new Schema({ # }] */ + memberCount: {type: Number, 'default': 0}, + challengeCount: {type: Number, 'default': 0}, balance: Number, logo: String, leaderMessage: String, @@ -59,6 +61,8 @@ function removeDuplicates(doc){ GroupSchema.pre('save', function(next){ removeDuplicates(this); + this.memberCount = _.size(this.members); + this.challengeCount = _.size(this.challenges); next(); }) diff --git a/views/options/profile.jade b/views/options/profile.jade index 8ee97c5fdb..7a223b6121 100644 --- a/views/options/profile.jade +++ b/views/options/profile.jade @@ -1,6 +1,6 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') .row-fluid - .span4.border-right + .span4 menu(type='list') // body-type li.customize-menu @@ -18,7 +18,7 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') button.f_armor_0_v1.customize-option(type='button', ng-click='set("preferences.armorSet","v1")') button.f_armor_0_v2.customize-option(type='button', ng-click='set("preferences.armorSet","v2")') - .span4.border-right + .span4 // hair li.customize-menu menu(label='Hair') @@ -28,7 +28,7 @@ script(id='partials/options.profile.avatar.html', type='text/ng-template') button(class='{{user.preferences.gender}}_hair_white customize-option', type='button', ng-click='set("preferences.hair","white")') button(class='{{user.preferences.gender}}_hair_red customize-option', type='button', ng-click='set("preferences.hair","red")') - .span4.border-right + .span4 // skin li.customize-menu menu(label='Basic Skins') From 219318b405d73ccc371ed9da847c437b3ad733f5 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Mon, 28 Oct 2013 18:20:21 -0700 Subject: [PATCH 13/15] challenges: remove username(), since we're now using user.profile.name --- src/controllers/groups.js | 2 +- views/options/groups/chat-message.jade | 2 +- views/options/groups/group.jade | 6 +++--- views/shared/header/avatar.jade | 2 +- views/shared/header/menu.jade | 2 +- views/shared/modals/members.jade | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 3b563e047d..65bce46e4d 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -201,7 +201,7 @@ api.postChat = function(req, res, next) { contributor: user.backer && user.backer.contributor, npc: user.backer && user.backer.npc, text: req.query.message, // FIXME this should be body, but ngResource is funky - user: helpers.username(user.auth, user.profile.name), + user: user.profile.name, timestamp: +(new Date) }; diff --git a/views/options/groups/chat-message.jade b/views/options/groups/chat-message.jade index 392627fbcc..af57fc283d 100644 --- a/views/options/groups/chat-message.jade +++ b/views/options/groups/chat-message.jade @@ -1,4 +1,4 @@ -li(ng-repeat='message in group.chat', ng-class='{highlight: message.text.indexOf(username(user.auth,user.profile.name)) != -1}') +li(ng-repeat='message in group.chat', ng-class='{highlight: indexOf(message.text, user.profile.name)}') a.label.chat-message(class='{{nameTagClasses(message)}}', tooltip='{{message.contributor}}', ng-click='clickMember(message.uuid, true)') | {{message.user}} span diff --git a/views/options/groups/group.jade b/views/options/groups/group.jade index cc32f3b157..317a4be77e 100644 --- a/views/options/groups/group.jade +++ b/views/options/groups/group.jade @@ -32,7 +32,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' h4 Assign Group Leader select#group-leader-selection - option(ng-repeat='member in group.members', selected='member._id == _new.groupLeader') {{username(member.auth,member.profile.name)}} + option(ng-repeat='member in group.members', selected='member._id == _new.groupLeader') {{member.profile.name}} button(x-bind='click:assignGroupLeader', ng-click='notPorted()') Assign div(ng-show='group.websites') @@ -68,7 +68,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' // {{/}} a(data-toggle='modal', data-target='#avatar-modal-{{member._id}}') span(ng-class='{"badge badge-info": group.leader==member._id}', ng-click='clickMember(member._id, true)') - | {{username(member.auth, member.profile.name)}} + | {{member.profile.name}} td ({{member._id}}) .modal-footer.whatever-options form.form-inline(ng-submit='invite(group, invitee)') @@ -107,7 +107,7 @@ a.pull-right.gem-wallet(popover-trigger='mouseenter', popover-title='Guild Bank' td .popover.static-popover.fade.right.in.wide-popover .arrow - h3.popover-title {{username(Members.members[group.leader].auth, Members.members[group.leader].profile.name)}} + h3.popover-title {{Members.members[group.leader].profile.name}} .popover-content markdown(ng-model='group.leaderMessage') div(ng-controller='ChatCtrl') diff --git a/views/shared/header/avatar.jade b/views/shared/header/avatar.jade index 1ba4dbd22d..d4846cffc5 100644 --- a/views/shared/header/avatar.jade +++ b/views/shared/header/avatar.jade @@ -1,4 +1,4 @@ -figure.herobox(ng-click='clickMember(profile._id)', data-name='{{username(profile.auth, profile.profile.name)}}', ng-class='{isUser: profile.id==user.id, hasPet: profile.items.pet}', data-level='{{profile.stats.lvl}}', data-uid='{{profile.id}}', rel='popover', data-placement='bottom', data-trigger='hover', data-html='true', data-content="
    Level: {{profile.stats.lvl}}
    GP: {{profile.stats.gp | number:0}}
    {{count(profile.items.pets)}} / 90 Pets Found
    ") +figure.herobox(ng-click='clickMember(profile._id)', data-name='{{profile.profile.name}}', ng-class='{isUser: profile.id==user.id, hasPet: profile.items.pet}', data-level='{{profile.stats.lvl}}', data-uid='{{profile.id}}', rel='popover', data-placement='bottom', data-trigger='hover', data-html='true', data-content="
    Level: {{profile.stats.lvl}}
    GP: {{profile.stats.gp | number:0}}
    {{count(profile.items.pets)}} / 90 Pets Found
    ") .character-sprites span(ng-class='{zzz:profile.flags.rest}') span(class='{{profile.preferences.gender}}_skin_{{profile.preferences.skin}}') diff --git a/views/shared/header/menu.jade b/views/shared/header/menu.jade index 5908bf27aa..1baf555a64 100644 --- a/views/shared/header/menu.jade +++ b/views/shared/header/menu.jade @@ -2,7 +2,7 @@ button.task-action-btn.tile.solid(ng-hide='authenticated()', style='cursor: pointer;', ng-click="modals.login = true") Login / Register ul.nav.site-nav(ng-show='authenticated()') li.flyout(ng-controller='MenuCtrl') - h1.task-action-btn.tile.solid.user-reporter {{username(user.auth, user.profile.name)}} + h1.task-action-btn.tile.solid.user-reporter {{user.profile.name}} ul.flyout-content.nav.stacked li a.task-action-btn.tile.solid diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 0605979ac7..715dc295d7 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -2,7 +2,7 @@ div(ng-controller='MemberModalCtrl') #memberModal(modal='modals.member') .modal-header h3 - span {{username(profile.auth, profile.profile.name)}} + span {{profile.profile.name}} span(ng-show='profile.backer.contributor') - {{profile.backer.contributor}} .modal-body .row-fluid From be657a76e0c54d6e6e2fcadfcfe4e9b064a1df72 Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 29 Oct 2013 12:26:13 -0700 Subject: [PATCH 14/15] challenges: pass down if user is member of group (group._isMember) for performance. some bug fixes --- public/js/app.js | 2 +- public/js/controllers/rootCtrl.js | 2 +- src/controllers/groups.js | 17 ++++++++++++++--- src/models/group.js | 1 + views/options/groups/index.jade | 6 +++--- 5 files changed, 20 insertions(+), 8 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index 9c38ba99dd..aa7f899e8d 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -77,7 +77,7 @@ window.habitrpg = angular.module('habitrpg', controller: 'PartyCtrl' }) .state('options.groups.guilds', { - url: '/party', + url: '/guilds', templateUrl: "partials/options.groups.guilds.html", controller: 'GuildsCtrl' }) diff --git a/public/js/controllers/rootCtrl.js b/public/js/controllers/rootCtrl.js index 3c4658d764..7c18fb7386 100644 --- a/public/js/controllers/rootCtrl.js +++ b/public/js/controllers/rootCtrl.js @@ -18,7 +18,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ // indexOf helper $scope.indexOf = function(haystack, needle){ - return ~haystack.indexOf(needle); + return haystack && ~haystack.indexOf(needle); } $scope.safeApply = function(fn) { diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 65bce46e4d..35383ed053 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -52,7 +52,6 @@ api.getGroups = function(req, res, next) { var fields = req.query.fields && req.query.fields.replace(/\,/g, ' ') var challengeFields = 'name description'; // public challenge fields, request specific challenge for more details - // First get all groups async.parallel({ party: function(cb) { @@ -64,6 +63,8 @@ api.getGroups = function(req, res, next) { query.populate('members', sanitizeMemberFields(req.query['fields.members'], partyFields)); if (~fields.indexOf('challenges')) query.populate('challenges', challengeFields); + } else { + query.populate('members', partyFields); } query.exec(function(err, group){ if (err) return cb(err); @@ -82,6 +83,8 @@ api.getGroups = function(req, res, next) { query.populate('members', sanitizeMemberFields(req.query['fields.members'], 'profile.name')); if (~fields.indexOf('challenges')) query.populate('challenges', challengeFields); + } else { + query.populate('members', 'profile.name'); } query.exec(cb); }, @@ -100,9 +103,17 @@ api.getGroups = function(req, res, next) { "public": function(cb) { if (type && !~type.indexOf('public')) return cb(null, []); Group.find({privacy: 'public'}) - .select('name description memberCount') + .select('name description memberCount members') .sort('-memberCount') - .exec(cb); + .exec(function(err, groups){ + if (err) return cb(err); + _.each(groups, function(g){ + // To save some client-side performance, don't send down the full members arr, just send down temp var _isMember + if (~g.members.indexOf(user._id)) g._isMember = true; + g.members = undefined; + }); + cb(null, groups); + }); } }, function(err, results){ diff --git a/src/models/group.js b/src/models/group.js index c297f31acc..0313a017b2 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -69,6 +69,7 @@ GroupSchema.pre('save', function(next){ GroupSchema.methods.toJSON = function(){ var doc = this.toObject(); removeDuplicates(doc); + doc._isMember = this._isMember; return doc; } diff --git a/views/options/groups/index.jade b/views/options/groups/index.jade index 6481d5e79f..0bd8241630 100644 --- a/views/options/groups/index.jade +++ b/views/options/groups/index.jade @@ -44,13 +44,13 @@ script(type='text/ng-template', id='partials/options.groups.guilds.html') tr(ng-repeat='group in groups.public | filter:guildSearch') td ul.pull-right.challenge-accordion-header-specs - li {{group.members.length}} member(s) + li {{group.memberCount}} member(s) li // join / leave - a.btn.btn-small.btn-danger(ng-show='isMember(user, group)', ng-click='leave(group)') + a.btn.btn-small.btn-danger(ng-show='group_.isMember', ng-click='leave(group)') i.icon-ban-circle | Leave - a.btn.btn-small.btn-success(ng-hide='isMember(user, group)', ng-click='join(group)') + a.btn.btn-small.btn-success(ng-hide='group._isMember', ng-click='join(group)') i.icon-ok | Join h4 {{group.name}} From d9d769a0e1c036c102ee7a86ecebfa69fe92f06a Mon Sep 17 00:00:00 2001 From: Tyler Renelle Date: Tue, 29 Oct 2013 15:25:50 -0700 Subject: [PATCH 15/15] challenges: much better implemntation of ui-router for nested groups. No need to specify fields or populate members at /groups - instead, provide that functionality at /groups/:gid --- public/js/app.js | 22 +++-- public/js/controllers/groupsCtrl.js | 1 - public/js/services/groupServices.js | 28 +++---- src/controllers/groups.js | 124 ++++++++++------------------ views/options/groups/index.jade | 76 +++++++++-------- 5 files changed, 110 insertions(+), 141 deletions(-) diff --git a/public/js/app.js b/public/js/app.js index aa7f899e8d..b6a5e14a97 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -16,6 +16,7 @@ window.habitrpg = angular.module('habitrpg', .when('/options', '/options/profile/avatar') .when('/options/profile', '/options/profile/avatar') .when('/options/groups', '/options/groups/tavern') + .when('/options/groups/guilds', '/options/groups/guilds/public') .when('/options/inventory', '/options/inventory/inventory') // redirect states that don't match @@ -64,12 +65,6 @@ window.habitrpg = angular.module('habitrpg', url: "/tavern", templateUrl: "partials/options.groups.tavern.html", controller: 'TavernCtrl' - // TODO this doesn't work, seems ngResource doesn't get the .then() function -// resolve: { -// group: ['Groups', function(Groups){ -// //return Groups.fetchTavern(); -// }] -// } }) .state('options.groups.party', { url: '/party', @@ -81,6 +76,21 @@ window.habitrpg = angular.module('habitrpg', templateUrl: "partials/options.groups.guilds.html", controller: 'GuildsCtrl' }) + .state('options.groups.guilds.public', { + url: '/public', + templateUrl: "partials/options.groups.guilds.public.html" + }) + .state('options.groups.guilds.create', { + url: '/create', + templateUrl: "partials/options.groups.guilds.create.html" + }) + .state('options.groups.guilds.detail', { + url: '/:gid', + templateUrl: 'partials/options.groups.guilds.detail.html', + controller: ['$scope', 'Groups', '$stateParams', function($scope, Groups, $stateParams){ + $scope.group = Groups.Group.get({gid:$stateParams.gid}); + }] + }) // Options > Inventory .state('options.inventory', { diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index b0d1fba4e8..8045fb697d 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -205,7 +205,6 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Groups', '$http', 'A function($scope, Groups, User) { Groups.fetchTavern(); $scope.group = Groups.groups.tavern; - $scope.rest = function(){ User.user.flags.rest = !User.user.flags.rest; User.log({op:'set',data:{'flags.rest':User.user.flags.rest}}); diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 8c6d9a4281..849be47b44 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -10,7 +10,7 @@ angular.module('groupServices', ['ngResource']). var Group = $resource(API_URL + '/api/v1/groups/:gid', {gid:'@_id', messageId: '@_messageId'}, { - //'query': {method: "GET", isArray:false} + query: {method: "GET", isArray:false}, postChat: {method: "POST", url: API_URL + '/api/v1/groups/:gid/chat'}, deleteChatMessage: {method: "DELETE", url: API_URL + '/api/v1/groups/:gid/chat/:messageId'}, join: {method: "POST", url: API_URL + '/api/v1/groups/:gid/join'}, @@ -34,32 +34,24 @@ angular.module('groupServices', ['ngResource']). }; // But we don't defer triggering Party, since we always need it for the header if nothing else - Group.query({type:'party', fields:'members'}, function(_groups){ - partyQ.resolve(_groups[0]); - Members.populate(_groups[0]); + Group.get({gid:'party'}, function(party){ + partyQ.resolve(party); }) return { // Note the _.once() to make sure it can never be called again fetchGuilds: _.once(function(){ - $('#loading-indicator').show(); - Group.query({type:'guilds'}, function(_groups){ - $('#loading-indicator').hide(); - guildsQ.resolve(_groups); - Members.populate(_groups); - }) - Group.query({type:'public'}, function(_groups){ - publicQ.resolve(_groups); - Members.populate(_groups); + Group.query(function(_groups){ + guildsQ.resolve(_groups.guilds); + Members.populate(_groups.guilds); + publicQ.resolve(_groups['public']); + Members.populate(_groups['public']); }) }), fetchTavern: _.once(function(){ - $('#loading-indicator').show(); - Group.query({type:'tavern'}, function(_groups){ - $('#loading-indicator').hide(); - Members.populate(_groups[0]); - tavernQ.resolve(_groups[0]); + Group.get({gid:'habitrpg'}, function(tavern){ + tavernQ.resolve(tavern); }) }), diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 35383ed053..10ff4ef7d2 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -18,6 +18,7 @@ var api = module.exports; var itemFields = 'items.armor items.head items.shield items.weapon items.currentPet'; var partyFields = 'profile preferences stats achievements party backer flags.rest auth.timestamps ' + itemFields; +var nameFields = 'profile.name'; function removeSelf(group, user){ group.members = _.filter(group.members, function(m){return m._id != user._id}); @@ -31,80 +32,31 @@ api.getMember = function(req, res) { }) } -function sanitizeMemberFields(fields, fallback) { - if (!fields) return fallback; - if (~fields.indexOf(',')) fields = fields.replace(/\,/g, ' '); - var intersection = _.intersection( (partyFields + ' items').split(' '), (fields).split(' ') ); - return _.isEmpty(_.filter(intersection, function(f){return f!=''})) ? fallback : intersection; -} - /** - * Get groups. If req.query.type provided, returned as an array (so ngResource can use). If not, returned as - * object {guilds, public, party, tavern}. req.query.type can be comma-separated `type=guilds,party` - * - * We can request specific fields to keep things slim. Not specified gives you full detail. Request them as: - * - simple: ?fields=title,description,chat,challenges,memberCount - * - sub-fields: ?fields=title,description,members&fields.members=profile.name + * Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually + * as /groups/party or /groups/tavern */ -api.getGroups = function(req, res, next) { +api.getGroups = function(req, res) { var user = res.locals.user; - var type = req.query.type && req.query.type.split(','); - var fields = req.query.fields && req.query.fields.replace(/\,/g, ' ') - var challengeFields = 'name description'; // public challenge fields, request specific challenge for more details + var groupFields = 'name description memberCount'; + var sort = '-memberCount'; async.parallel({ - party: function(cb) { - if (type && !~type.indexOf('party')) return cb(null, {}); - var query = Group.findOne({type: 'party', members: {'$in': [user._id]}}); - if (fields) { - query.select(fields); - if (~fields.indexOf('members')) - query.populate('members', sanitizeMemberFields(req.query['fields.members'], partyFields)); - if (~fields.indexOf('challenges')) - query.populate('challenges', challengeFields); - } else { - query.populate('members', partyFields); - } - query.exec(function(err, group){ - if (err) return cb(err); - // Remove self from party (match in `populate()` hangs - mongoose bug) - removeSelf(group, user); - cb(null, group); - }); + // unecessary given our ui-router setup + party: function(cb){ + return cb(null, [{}]); }, guilds: function(cb) { - if (type && !~type.indexOf('guilds')) return cb(null, {}); - var query = Group.find({type: 'guild', members: {'$in': [user._id]}}); - if (fields) { - query.select(fields); - if (~fields.indexOf('members')) - query.populate('members', sanitizeMemberFields(req.query['fields.members'], 'profile.name')); - if (~fields.indexOf('challenges')) - query.populate('challenges', challengeFields); - } else { - query.populate('members', 'profile.name'); - } - query.exec(cb); + Group.find({members: {'$in': [user._id]}, type:'guild'}) + .select(groupFields).sort(sort).exec(cb); }, - tavern: function(cb) { - if (type && !~type.indexOf('tavern')) return cb(null, {}); - var query = Group.findById('habitrpg'); - if (fields) { - query.select(fields); - if (~fields.indexOf('challenges')) - query.populate('challenges', challengeFields); - } - query.exec(cb); - }, - - "public": function(cb) { - if (type && !~type.indexOf('public')) return cb(null, []); + 'public': function(cb) { Group.find({privacy: 'public'}) - .select('name description memberCount members') - .sort('-memberCount') + .select(groupFields + ' members') + .sort(sort) .exec(function(err, groups){ if (err) return cb(err); _.each(groups, function(g){ @@ -114,41 +66,49 @@ api.getGroups = function(req, res, next) { }); cb(null, groups); }); + }, + + // unecessary given our ui-router setup + tavern: function(cb) { + return cb(null, [{}]); } }, function(err, results){ if (err) return res.json(500, {err: err}); - - // If they're requesting a specific type, let's return it as an array so that $ngResource - // can utilize it properly - if (type) { - results = _.reduce(type, function(m,t){ - return m.concat(_.isArray(results[t]) ? results[t] : [results[t]]); - }, []); - } res.json(results); }) }; /** * Get group + * TODO: implement requesting fields ?fields=chat,members */ -api.getGroup = function(req, res, next) { +api.getGroup = function(req, res) { var user = res.locals.user; var gid = req.params.gid; - Group.findById(gid).populate('members', partyFields).exec(function(err, group){ - if ( (group.type == 'guild' && group.privacy == 'private') || group.type == 'party') { - if(!_.find(group.members, {_id: user._id})) - return res.json(401, {err: "You don't have access to this group"}); - } - // Remove self from party (see above failing `match` directive in `populate` - if (group.type == 'party') { - removeSelf(group, user); - } - res.json(group); + // This will be called for the header, we need extra members' details than usuals + if (gid == 'party') { + Group.findOne({type: 'party', members: {'$in': [user._id]}}) + .populate('members', partyFields).exec(function(err, group){ + if (err) return res.json(500,{err:err}); + removeSelf(group, user); + res.json(group); + }); + } else { + Group.findById(gid).populate('members', nameFields).exec(function(err, group){ + if ( (group.type == 'guild' && group.privacy == 'private') || group.type == 'party') { + if(!_.find(group.members, {_id: user._id})) + return res.json(401, {err: "You don't have access to this group"}); + } + // Remove self from party (see above failing `match` directive in `populate` + if (group.type == 'party') { + removeSelf(group, user); + } + res.json(group); + }) + } - }) }; diff --git a/views/options/groups/index.jade b/views/options/groups/index.jade index 0bd8241630..e1541bc448 100644 --- a/views/options/groups/index.jade +++ b/views/options/groups/index.jade @@ -23,43 +23,51 @@ script(type='text/ng-template', id='partials/options.groups.party.html') {{user.id}} include ./create-group +script(type='text/ng-template', id='partials/options.groups.guilds.public.html') + div(ng-repeat='invitation in user.invitations.guilds') + h3 You're Invited To {{invitation.name}} + a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept + a.btn.btn-danger(ng-click='reject(invitation)') Reject + // Public Groups + .options-group.option-large.whatever-options + input.option-content(type='text',ng-model='guildSearch', placeholder='Search') + table.table.table-striped + tr(ng-repeat='group in groups.public | filter:guildSearch') + td + ul.pull-right.challenge-accordion-header-specs + li {{group.memberCount}} member(s) + li + // join / leave + a.btn.btn-small.btn-danger(ng-show='group_.isMember', ng-click='leave(group)') + i.icon-ban-circle + | Leave + a.btn.btn-small.btn-success(ng-hide='group._isMember', ng-click='join(group)') + i.icon-ok + | Join + h4 {{group.name}} + p {{group.description}} + +script(type='text/ng-template', id='partials/options.groups.guilds.detail.html') + include ./group + +script(type='text/ng-template', id='partials/options.groups.guilds.create.html') + include ./create-group + script(type='text/ng-template', id='partials/options.groups.guilds.html') ul.nav.nav-tabs - li.active - a(data-target='#groups-public-guilds', data-toggle='tab') Public Guilds - li(ng-repeat='group in groups.guilds') - a(data-target='#groups-guild-{{group._id}}', data-toggle='tab') {{group.name}} - li - a(data-target='#groups-create-guild', data-toggle='tab') Create Guild - .tab-content - .tab-pane.active#groups-public-guilds - div(ng-repeat='invitation in user.invitations.guilds') - h3 You're Invited To {{invitation.name}} - a.btn.btn-success(data-type='guild', ng-click='join(invitation)') Accept - a.btn.btn-danger(ng-click='reject(invitation)') Reject - // Public Groups - .options-group.option-large.whatever-options - input.option-content(type='text',ng-model='guildSearch', placeholder='Search') - table.table.table-striped - tr(ng-repeat='group in groups.public | filter:guildSearch') - td - ul.pull-right.challenge-accordion-header-specs - li {{group.memberCount}} member(s) - li - // join / leave - a.btn.btn-small.btn-danger(ng-show='group_.isMember', ng-click='leave(group)') - i.icon-ban-circle - | Leave - a.btn.btn-small.btn-success(ng-hide='group._isMember', ng-click='join(group)') - i.icon-ok - | Join - h4 {{group.name}} - p {{group.description}} - .tab-pane(id='groups-guild-{{group._id}}', ng-repeat='group in groups.guilds') - include ./group + li(ng-class="{ active: $state.includes('options.groups.guilds.public') }") + a(ui-sref='options.groups.guilds.public') + | Public Guilds + li(ng-class="{ active: $stateParams.gid == group._id }", ng-repeat='group in groups.guilds') + a(ui-sref="options.groups.guilds.detail({gid:group._id})") + | {{group.name}} + li(ng-class="{ active: $state.includes('options.groups.guilds.create') }") + a(ui-sref='options.groups.guilds.create') + | Create Guild - .tab-pane#groups-create-guild - include ./create-group + .tab-content + .tab-pane.active + div(ui-view) script(type='text/ng-template', id='partials/options.groups.html') ul.nav.nav-tabs