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
This commit is contained in:
Tyler Renelle 2013-10-26 17:23:52 -07:00
parent e81193ef28
commit e45d8307e7
18 changed files with 415 additions and 575 deletions

View file

@ -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'});

View file

@ -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}})

View file

@ -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') {

View file

@ -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;
};
}]);

View file

@ -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 <ins>, 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({});
}
}]);

View file

@ -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;
}
}
}]);

View file

@ -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.

View file

@ -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;
});
};

41
src/models/task.js Normal file
View file

@ -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);

View file

@ -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();

View file

@ -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) {

View file

@ -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

4
views/main/index.jade Normal file
View file

@ -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')

View file

@ -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
// <li><a data-toggle='tab' data-target="#profile-challenges" id='profile-challenges-tab-link' ><i class='icon-bullhorn'></i> Challenges</a></li>
// app:challenges:main
.tab-pane#settings-tab
include ./settings
.tab-pane#settings-tab
include ./settings

View file

@ -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')
// <a href="https://www.google.com/calendar/render?cid={{encodeiCalLink(_user.id, _user.apiToken)}}" rel=tooltip title="Google Calendar"><i class=icon-calendar></i></a>
// 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

View file

@ -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)}}
// <i class='icon-bullhorn' style='background-color:red;' x-bind=click:tasks.toggleTaskEdit rel=tooltip title="Broken Challenge Link"></i>
// {{else}}
// <i class='icon-bullhorn' rel=tooltip title="Challenge Task"></i>
// {{/}}
// {{else}}
// <!-- delete -->
// <a x-bind="click:tasks.del" rel=tooltip title="Delete"><i class="icon-trash"></i></a>
// {{/}}
//- {{#if :task.challenge}}
//- {{#if brokenChallengeLink(:task)}}
//- <i class='icon-bullhorn' style='background-color:red;' x-bind=click:tasks.toggleTaskEdit rel=tooltip title="Broken Challenge Link"></i>
//- {{else}}
//- <i class='icon-bullhorn' rel=tooltip title="Challenge Task"></i>
//- {{/}}
//- {{else}}
//- <!-- delete -->
//- <a x-bind="click:tasks.del" rel=tooltip title="Delete"><i class="icon-trash"></i></a>
//- {{/}}
// 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
// {{/}}
// </div>
// {/}
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]')

View file

@ -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({});