diff --git a/common/locales/en/challenge.json b/common/locales/en/challenge.json index ff0a4e8e6e..51d55ae69c 100644 --- a/common/locales/en/challenge.json +++ b/common/locales/en/challenge.json @@ -54,5 +54,6 @@ "challengedNotOwnedFilter": "Not Owned", "challengedEitherOwnedFilter": "Either", "backToChallenges": "Back to all challenges", - "prizeValue": "<%= gemcount %> <%= gemicon %> Prize" + "prizeValue": "<%= gemcount %> <%= gemicon %> Prize", + "clone": "Clone" } diff --git a/test/spec/controllers/challengesCtrlSpec.js b/test/spec/controllers/challengesCtrlSpec.js index 1982786609..f3bc73ae7c 100644 --- a/test/spec/controllers/challengesCtrlSpec.js +++ b/test/spec/controllers/challengesCtrlSpec.js @@ -182,4 +182,80 @@ describe('Challenges Controller', function() { }); }); }); + + describe('clone', function() { + + var challengeToClone = { + name: 'copyChallenge', + description: 'copyChallenge', + habits: [specHelper.newHabit()], + dailys: [specHelper.newDaily()], + todos: [specHelper.newTodo()], + rewards: [specHelper.newReward()], + leader: 'unique-user-id', + group: { _id: "copyGroup" }, + timestamp: new Date("October 13, 2014 11:13:00"), + members: ['id', 'another-id'], + official: true, + _isMember: true, + prize: 1 + }; + + it('Clones the basic challenge info', function() { + + scope.clone(challengeToClone); + + expect(scope.newChallenge.name).to.eql(challengeToClone.name); + expect(scope.newChallenge.shortName).to.eql(challengeToClone.shortName); + expect(scope.newChallenge.description).to.eql(challengeToClone.description); + expect(scope.newChallenge.leader).to.eql(user._id); + expect(scope.newChallenge.group).to.eql(challengeToClone.group._id); + expect(scope.newChallenge.official).to.eql(challengeToClone.official); + expect(scope.newChallenge.prize).to.eql(challengeToClone.prize); + }); + + it('does not clone members', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.members).to.not.exist; + }); + + it('does not clone timestamp', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.timestamp).to.not.exist; + }); + + it('clones habits', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.habits.length).to.eql(challengeToClone.habits.length); + expect(scope.newChallenge.habits[0].text).to.eql(challengeToClone.habits[0].text); + expect(scope.newChallenge.habits[0].notes).to.eql(challengeToClone.habits[0].notes); + }); + + it('clones dailys', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.dailys.length).to.eql(challengeToClone.dailys.length); + expect(scope.newChallenge.dailys[0].text).to.eql(challengeToClone.dailys[0].text); + expect(scope.newChallenge.dailys[0].notes).to.eql(challengeToClone.dailys[0].notes); + }); + + it('clones todos', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.todos.length).to.eql(challengeToClone.todos.length); + expect(scope.newChallenge.todos[0].text).to.eql(challengeToClone.todos[0].text); + expect(scope.newChallenge.todos[0].notes).to.eql(challengeToClone.todos[0].notes); + }); + + it('clones rewards', function() { + scope.clone(challengeToClone); + + expect(scope.newChallenge.rewards.length).to.eql(challengeToClone.rewards.length); + expect(scope.newChallenge.rewards[0].text).to.eql(challengeToClone.rewards[0].text); + expect(scope.newChallenge.rewards[0].notes).to.eql(challengeToClone.rewards[0].notes); + }); + }); }); diff --git a/test/spec/services/taskServicesSpec.js b/test/spec/services/taskServicesSpec.js index fe6cee12cb..212ba816f9 100644 --- a/test/spec/services/taskServicesSpec.js +++ b/test/spec/services/taskServicesSpec.js @@ -22,7 +22,7 @@ describe('Tasks Service', function() { var task; beforeEach(function(){ - task = { id: 'task-id' }; // @TODO: replace with task factory + task = specHelper.newTask(); }); it('toggles the _editing property', function() { @@ -66,4 +66,216 @@ describe('Tasks Service', function() { expect(rootScope.charts[task.id]).to.eql(false); }); }); + + describe('cloneTask', function() { + + context('generic tasks', function() { + it('clones the data from a task', function() { + var task = specHelper.newTask(); + var clonedTask = tasks.cloneTask(task); + + expect(clonedTask.text).to.eql(task.text); + expect(clonedTask.notes).to.eql(task.notes); + expect(clonedTask.tags.includedTag).to.eql(task.tags.includedTag); + expect(clonedTask.tags.notIncludedTag).to.eql(task.tags.notIncludedTag); + expect(clonedTask.priority).to.eql(task.priority); + expect(clonedTask.attribute).to.eql(task.attribute); + }); + + it('does not clone original task\'s id or _id', function() { + var task = specHelper.newTask(); + var clonedTask = tasks.cloneTask(task); + + expect(clonedTask.id).to.exist; + expect(clonedTask.id).to.not.eql(task.id); + expect(clonedTask._id).to.exist; + expect(clonedTask._id).to.not.eql(task._id); + }); + + it('does not clone original task\'s dateCreated attribute', function() { + var task = specHelper.newTask({ + dateCreated: new Date(2014, 5, 1, 1, 1, 1, 1), + }); + var clonedTask = tasks.cloneTask(task); + + expect(clonedTask.dateCreated).to.exist; + expect(clonedTask.dateCreated).to.not.eql(task.dateCreated); + }); + + it('does not clone original task\'s value', function() { + var task = specHelper.newTask({ + value: 130 + }); + var clonedTask = tasks.cloneTask(task); + + expect(clonedTask.value).to.exist; + expect(clonedTask.value).to.not.eql(task.value); + }); + }); + + context('Habits', function() { + + it('clones a habit', function() { + var habit = specHelper.newHabit({ + up: true, + down: false + }); + var clonedHabit = tasks.cloneTask(habit); + + expect(clonedHabit.type).to.eql('habit'); + expect(clonedHabit.up).to.eql(habit.up); + expect(clonedHabit.down).to.eql(habit.down); + }); + }); + + context('Dailys', function() { + + it('clones a daily', function() { + var daily = specHelper.newDaily({ + frequency: 'daily', + everyX: 3, + startDate: new Date(2014, 5, 1, 1, 1, 1, 1), + }); + + var clonedDaily = tasks.cloneTask(daily); + + expect(clonedDaily.type).to.eql('daily'); + expect(clonedDaily.frequency).to.eql(daily.frequency); + expect(clonedDaily.everyX).to.eql(daily.everyX); + expect(clonedDaily.startDate).to.eql(daily.startDate); + }); + + it('does not clone streak', function() { + var daily = specHelper.newDaily({ + streak: 11 + }); + + var clonedDaily = tasks.cloneTask(daily); + + expect(clonedDaily.streak).to.eql(0); + }); + }); + + context('Todos', function() { + + it('clones a todo', function() { + var todo = specHelper.newTodo(); + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.type).to.eql('todo'); + }); + + it('does not clone due date', function() { + var todo = specHelper.newTodo({ + date: '2015-06-20' + }); + + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.date).to.not.exist; + }); + + it('does not clone date completed', function() { + var todo = specHelper.newTodo({ + dateCompleted: new Date() + }); + + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.dateCompleted).to.not.exist; + }); + }); + + context('Rewards', function() { + + it('clones a reward', function() { + var reward = specHelper.newReward(); + var clonedReward = tasks.cloneTask(reward); + + expect(clonedReward.type).to.eql('reward'); + }); + + it('does clone a reward\'s vaue', function() { + var reward = specHelper.newReward({ + value: 20 + }); + var clonedReward = tasks.cloneTask(reward); + + expect(clonedReward.value).to.eql(reward.value); + }); + }); + + context('complete', function() { + it('does not clone completed status', function() { + var todo = specHelper.newTodo({ + completed: true + }); + + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.completed).to.eql(false); + }); + }); + + context('history', function() { + + it('does not clone history', function() { + var habit = specHelper.newHabit({ + history: [ + { date: Date.now, value: 3.1 }, + { date: Date.now, value: 2.7 } + ] + }); + var clonedHabit = tasks.cloneTask(habit); + + expect(clonedHabit.history).to.be.an.array; + expect(clonedHabit.history).to.be.empty; + }); + }); + + context('checklists', function() { + + it('clones checklist text', function() { + var todo = specHelper.newTodo({ + checklist: [{ + completed: true, + text: 'checklist 1', + id: 'cl-1' + }, { + completed: false, + text: 'checklist 2', + id: 'cl-2' + }] + }); + + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.checklist[0].text).to.eql(todo.checklist[0].text); + expect(clonedTodo.checklist[1].text).to.eql(todo.checklist[1].text); + }); + + it('does not clone complete or id attribute of checklist', function() { + var todo = specHelper.newTodo({ + checklist: [{ + completed: true, + text: 'checklist 1', + id: 'cl-1' + }, { + completed: false, + text: 'checklist 2', + id: 'cl-2' + }] + }); + + var clonedTodo = tasks.cloneTask(todo); + + expect(clonedTodo.checklist[0].completed).to.eql(false); + expect(clonedTodo.checklist[0].id).to.not.eql(todo.checklist[0].id); + expect(clonedTodo.checklist[0].id).to.exist; + expect(clonedTodo.checklist[1].completed).to.eql(false); + expect(clonedTodo.checklist[1].id).to.not.eql(todo.checklist[1].id); + expect(clonedTodo.checklist[1].id).to.exist; + }); + }); + }); }); diff --git a/test/spec/specHelper.js b/test/spec/specHelper.js index 80f5858fe8..60a638024f 100644 --- a/test/spec/specHelper.js +++ b/test/spec/specHelper.js @@ -1,7 +1,18 @@ beforeEach(module('habitrpg')); -specHelper = { - newUser: function(){ +var specHelper = {}; + +(function(){ + + specHelper.newUser = newUser; + specHelper.newGroup = newGroup; + specHelper.newTask = newTask; + specHelper.newHabit = newHabit; + specHelper.newDaily = newDaily; + specHelper.newTodo = newTodo; + specHelper.newReward = newReward; + + function newUser() { var buffs = {per:0, int:0, con:0, str:0, stealth: 0, streaks: false}; user = { auth:{timestamps: {}}, @@ -29,8 +40,9 @@ specHelper = { achievements: {}, }; return user; - }, - newGroup: function(leader) { + } + + function newGroup(leader) { var quest = { progress: { }, active: false }; group = { "leader" : leader, @@ -45,5 +57,91 @@ specHelper = { }; return group; } -}; + function newTask(overrides) { + var task = { + id: 'task-id', + _id: 'task-id', + dateCreated: Date.now, + text: 'task text', + notes: 'task notes', + tags: { }, + value: 0, + priority: 1, + attribute: 'str', + challenge: { } + }; + + for(var key in overrides) { + task[key] = overrides[key]; + } + + return task; + } + + function newHabit(overrides) { + var habit = newTask(); + habit.type = 'habit'; + habit.history = []; + habit.up = true; + habit.down = true; + + for(var key in overrides) { + habit[key] = overrides[key]; + } + + return habit; + } + + function newDaily(overrides) { + var daily = newTask(); + daily.type = 'daily'; + daily.frequency = 'weekly'; + daily.repeat = { + m: true, + t: true, + w: true, + th: true, + f: true, + s: true, + su: true + }; + daily.startDate = Date.now; + daily.history = []; + daily.completed = false; + daily.collapseChecklist = false; + daily.checklist = []; + daily.streak = 0; + + for(var key in overrides) { + daily[key] = overrides[key]; + } + + return daily; + } + + function newTodo(overrides) { + var todo = newTask(); + todo.type = 'todo'; + todo.completed = false; + todo.collapseChecklist = false; + todo.checklist = []; + + for(var key in overrides) { + todo[key] = overrides[key]; + } + + return todo; + } + + function newReward(overrides) { + var reward = newTask(); + reward.type = 'reward'; + + for(var key in overrides) { + reward[key] = overrides[key]; + } + + return reward; + } +})(); diff --git a/website/public/js/controllers/challengesCtrl.js b/website/public/js/controllers/challengesCtrl.js index a9d30bc506..a7f3f64ec6 100644 --- a/website/public/js/controllers/challengesCtrl.js +++ b/website/public/js/controllers/challengesCtrl.js @@ -89,6 +89,41 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User', }); }; + /** + * Clone + */ + $scope.clone = function(challenge) { + var clonedTasks = { + habit: [], + daily: [], + todo: [], + reward: [] + }; + + _(clonedTasks).each(function(val, type) { + challenge[type + 's'].forEach(_cloneTaskAndPush); + }); + + $scope.obj = $scope.newChallenge = new Challenges.Challenge({ + name: challenge.name, + shortName: challenge.shortName, + description: challenge.description, + habits: clonedTasks.habit, + dailys: clonedTasks.daily, + todos: clonedTasks.todo, + rewards: clonedTasks.reward, + leader: User.user._id, + group: challenge.group._id, + official: challenge.official, + prize: challenge.prize + }); + + function _cloneTaskAndPush(taskToClone) { + var task = Tasks.cloneTask(taskToClone); + clonedTasks[task.type].push(task); + } + }; + /** * Save */ diff --git a/website/public/js/services/taskServices.js b/website/public/js/services/taskServices.js index ac13b70641..3f968dfa6e 100644 --- a/website/public/js/services/taskServices.js +++ b/website/public/js/services/taskServices.js @@ -1,24 +1,53 @@ 'use strict'; -angular - .module('habitrpg') - .factory('Tasks', tasksFactory); +(function(){ + var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'dateCreated', 'history', 'id', 'streak']; -tasksFactory.$inject = [ - '$rootScope', - 'User' -]; + angular + .module('habitrpg') + .factory('Tasks', tasksFactory); -function tasksFactory($rootScope, User) { + tasksFactory.$inject = [ + '$rootScope', + 'Shared', + 'User' + ]; - function editTask(task) { - task._editing = !task._editing; - task._tags = !User.user.preferences.tagsCollapsed; - task._advanced = !User.user.preferences.advancedCollapsed; - if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; + function tasksFactory($rootScope, Shared, User) { + + function editTask(task) { + task._editing = !task._editing; + task._tags = !User.user.preferences.tagsCollapsed; + task._advanced = !User.user.preferences.advancedCollapsed; + if($rootScope.charts[task.id]) $rootScope.charts[task.id] = false; + } + + function cloneTask(task) { + var clonedTask = _.cloneDeep(task); + clonedTask = _cleanUpTask(clonedTask); + + return Shared.taskDefaults(clonedTask); + } + + function _cleanUpTask(task) { + var cleansedTask = _.omit(task, TASK_KEYS_TO_REMOVE); + + // Copy checklists but reset to uncomplete and assign new id + _(cleansedTask.checklist).forEach(function(item) { + item.completed = false; + item.id = Shared.uuid(); + }); + + if (cleansedTask.type !== 'reward') { + delete cleansedTask.value; + } + + return cleansedTask; + } + + return { + editTask: editTask, + cloneTask: cloneTask + }; } - - return { - editTask: editTask - }; -} +})(); diff --git a/website/views/options/social/challenges.jade b/website/views/options/social/challenges.jade index 4791fd0170..df5bc66f52 100644 --- a/website/views/options/social/challenges.jade +++ b/website/views/options/social/challenges.jade @@ -25,6 +25,7 @@ script(type='text/ng-template', id='partials/options.social.challenges.detail.ht div(bindonce='challenge', ng-if='challenge.leader._id==user._id') div(ng-hide='challenge._locked==false') button.btn.btn-sm.btn-default(ng-click='edit(challenge)')=env.t('edit') + button.btn.btn-sm.btn-success(ng-click='clone(challenge)')=env.t('clone') button.btn.btn-sm.btn-warning(ng-click='close(challenge, $event)', tooltip=env.t('deleteOrSelect'))=env.t('endChallenge') form(ng-show='challenge._locked==false')