mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-10-27 11:12:28 +01:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1343fdfb | ||
|
|
7297a6fa76 | ||
|
|
f713bf53c1 | ||
|
|
506705d738 | ||
|
|
cb1d385755 | ||
|
|
d8051f3bf1 | ||
|
|
042ac16c48 | ||
|
|
adce786ae2 | ||
|
|
a771c43989 | ||
|
|
0008839996 | ||
|
|
1e270e417c | ||
|
|
048d55b962 | ||
|
|
91e16ff191 | ||
|
|
c7d3539815 | ||
|
|
2ca52785e1 | ||
|
|
67e68ece29 | ||
|
|
9855f8c385 | ||
|
|
35544f14c5 | ||
|
|
6c017b31fb | ||
|
|
ac77ceb75f | ||
|
|
6ce2f53503 | ||
|
|
70d7a922f6 | ||
|
|
64b3896797 | ||
|
|
02d075e342 | ||
|
|
8faf73084a | ||
|
|
810355d2a5 | ||
|
|
aaf2ff5b70 | ||
|
|
977d2ae525 | ||
|
|
f9b759ae57 | ||
|
|
1f3bd45471 | ||
|
|
b1519eed14 | ||
|
|
f1286762a8 | ||
|
|
770ffe93fc | ||
|
|
2ab76db27c | ||
|
|
a099c1b3b5 | ||
|
|
d4287e1fd8 | ||
|
|
80323120b6 |
@@ -8,7 +8,7 @@ import content from './content/index';
|
||||
|
||||
const DROP_ANIMALS = keys(content.pets);
|
||||
|
||||
function beastMasterProgress (pets) {
|
||||
function beastMasterProgress (pets = {}) {
|
||||
let count = 0;
|
||||
|
||||
each(DROP_ANIMALS, (animal) => {
|
||||
@@ -19,7 +19,7 @@ function beastMasterProgress (pets) {
|
||||
return count;
|
||||
}
|
||||
|
||||
function dropPetsCurrentlyOwned (pets) {
|
||||
function dropPetsCurrentlyOwned (pets = {}) {
|
||||
let count = 0;
|
||||
|
||||
each(DROP_ANIMALS, (animal) => {
|
||||
@@ -30,7 +30,7 @@ function dropPetsCurrentlyOwned (pets) {
|
||||
return count;
|
||||
}
|
||||
|
||||
function mountMasterProgress (mounts) {
|
||||
function mountMasterProgress (mounts = {}) {
|
||||
let count = 0;
|
||||
|
||||
each(DROP_ANIMALS, (animal) => {
|
||||
@@ -41,7 +41,7 @@ function mountMasterProgress (mounts) {
|
||||
return count;
|
||||
}
|
||||
|
||||
function remainingGearInSet (userGear, set) {
|
||||
function remainingGearInSet (userGear = {}, set) {
|
||||
let gear = filter(content.gear.flat, (item) => {
|
||||
let setMatches = item.klass === set;
|
||||
let hasItem = userGear[item.key];
|
||||
@@ -54,7 +54,7 @@ function remainingGearInSet (userGear, set) {
|
||||
return count;
|
||||
}
|
||||
|
||||
function questsOfCategory (userQuests, category) {
|
||||
function questsOfCategory (userQuests = {}, category) {
|
||||
let quests = filter(content.quests, (quest) => {
|
||||
let categoryMatches = quest.category === category;
|
||||
let hasQuest = userQuests[quest.key];
|
||||
|
||||
@@ -15,7 +15,16 @@ module.exports = function ultimateGear (user) {
|
||||
}
|
||||
});
|
||||
|
||||
if (_.contains(user.achievements.ultimateGearSets, true) && user.flags.armoireEnabled !== true) {
|
||||
let ultimateGearSetValues;
|
||||
if (user.achievements.ultimateGearSets.toObject) {
|
||||
ultimateGearSetValues = Object.values(user.achievements.ultimateGearSets.toObject());
|
||||
} else {
|
||||
ultimateGearSetValues = Object.values(user.achievements.ultimateGearSets);
|
||||
}
|
||||
|
||||
let hasFullSet = _.includes(ultimateGearSetValues, true);
|
||||
|
||||
if (hasFullSet && user.flags.armoireEnabled !== true) {
|
||||
user.flags.armoireEnabled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = function sortTask (user, req = {}) {
|
||||
if (index === -1) {
|
||||
throw new NotFound(i18n.t('messageTaskNotFound', req.language));
|
||||
}
|
||||
if (!to && !fromParam) {
|
||||
if (to == null && fromParam == null) { // eslint-disable-line eqeqeq
|
||||
throw new BadRequest('?to=__&from=__ are required');
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
"NODE_DB_URI":"mongodb://localhost/habitrpg",
|
||||
"TEST_DB_URI":"mongodb://localhost/habitrpg_test",
|
||||
"NODE_ENV":"development",
|
||||
"ENABLE_CONSOLE_LOGS_IN_TEST": false,
|
||||
"CRON_SAFE_MODE":"false",
|
||||
"CRON_SEMI_SAFE_MODE":"false",
|
||||
"MAINTENANCE_MODE": "false",
|
||||
"SESSION_SECRET":"YOUR SECRET HERE",
|
||||
"ADMIN_EMAIL": "you@example.com",
|
||||
|
||||
@@ -109,7 +109,7 @@ function processGroups (afterId) {
|
||||
oldGroup.memberCount = oldGroup.members ? oldGroup.members.length : 0;
|
||||
oldGroup.challengeCount = oldGroup.challenges ? oldGroup.challenges.length : 0;
|
||||
|
||||
if (!oldGroup.balance <= 0) oldGroup.balance = 0;
|
||||
if (oldGroup.balance <= 0) oldGroup.balance = 0;
|
||||
if (!oldGroup.name) oldGroup.name = 'group name';
|
||||
if (!oldGroup.leaderOnly) oldGroup.leaderOnly = {};
|
||||
if (!oldGroup.leaderOnly.challenges) oldGroup.leaderOnly.challenges = false;
|
||||
|
||||
@@ -69,7 +69,25 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
expect(res[0].profile).to.have.all.keys(['name']);
|
||||
});
|
||||
|
||||
it('returns only first 30 members', async () => {
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not true', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await generateChallenge(user, group);
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only first 30 members if req.query.includeAllMembers is not defined', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -87,6 +105,24 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns all members if req.query.includeAllMembers is true', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await generateChallenge(user, group);
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
usersToGenerate.push(generateUser({challenges: [challenge._id]}));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
|
||||
expect(res.length).to.equal(32);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more members', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let challenge = await generateChallenge(user, group);
|
||||
|
||||
@@ -117,7 +117,7 @@ describe('POST /challenges/:challengeId/leave', () => {
|
||||
});
|
||||
|
||||
expect(testTask).to.not.be.undefined;
|
||||
expect(testTask.challenge).to.be.undefined;
|
||||
expect(testTask.challenge).to.eql({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
xdescribe('POST /debug/make-admin (pended for v3 prod testing)', () => {
|
||||
describe('POST /debug/make-admin (pended for v3 prod testing)', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
|
||||
@@ -74,6 +74,23 @@ describe('GET /groups/:groupId/members', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns only first 30 members even when ?includeAllMembers=true', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
|
||||
let usersToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
usersToGenerate.push(generateUser({party: {_id: group._id}}));
|
||||
}
|
||||
await Promise.all(usersToGenerate);
|
||||
|
||||
let res = await user.get('/groups/party/members?includeAllMembers=true');
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
||||
expect(member.profile).to.have.all.keys(['name']);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports using req.query.lastId to get more members', async () => {
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(savedTask.history).to.eql(task.history);
|
||||
expect(savedTask.createdAt).to.equal(task.createdAt);
|
||||
expect(savedTask.updatedAt).to.be.greaterThan(task.updatedAt);
|
||||
expect(savedTask.challenge).to.equal(task.challenge);
|
||||
expect(savedTask.challenge).to.eql(task.challenge);
|
||||
expect(savedTask.completed).to.eql(task.completed);
|
||||
expect(savedTask.streak).to.equal(savedTask.streak); // it's an habit, dailies can change it
|
||||
expect(savedTask.dateCompleted).to.equal(task.dateCompleted);
|
||||
|
||||
@@ -50,4 +50,43 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
message: t('wrongPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when password is missing', async () => {
|
||||
let body = {
|
||||
newPassword,
|
||||
confirmPassword: newPassword,
|
||||
};
|
||||
|
||||
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when newPassword is missing', async () => {
|
||||
let body = {
|
||||
password,
|
||||
confirmPassword: newPassword,
|
||||
};
|
||||
|
||||
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when confirmPassword is missing', async () => {
|
||||
let body = {
|
||||
password,
|
||||
newPassword,
|
||||
};
|
||||
|
||||
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@ describe('shared.fns.ultimateGear', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
user.achievements.ultimateGearSets.toObject = function () {
|
||||
return this;
|
||||
};
|
||||
});
|
||||
|
||||
it('sets armoirEnabled when partial achievement already achieved', () => {
|
||||
@@ -30,4 +33,25 @@ describe('shared.fns.ultimateGear', () => {
|
||||
ultimateGear(user);
|
||||
expect(user.flags.armoireEnabled).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not set armoirEnabled when gear is not owned', () => {
|
||||
let items = {
|
||||
gear: {
|
||||
owned: {
|
||||
toObject: () => {
|
||||
return {
|
||||
armor_warrior_5: true, // eslint-disable-line camelcase
|
||||
shield_warrior_5: true, // eslint-disable-line camelcase
|
||||
head_warrior_5: true, // eslint-disable-line camelcase
|
||||
weapon_warrior_6: false, // eslint-disable-line camelcase
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
user.items = items;
|
||||
ultimateGear(user);
|
||||
expect(user.flags.armoireEnabled).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('memberServices', function() {
|
||||
|
||||
it('calls get challenge members', function() {
|
||||
var challengeId = 1;
|
||||
var memberUrl = apiV3Prefix + '/challenges/' + challengeId + '/members';
|
||||
var memberUrl = apiV3Prefix + '/challenges/' + challengeId + '/members?includeAllMembers=true';
|
||||
$httpBackend.expectGET(memberUrl).respond({});
|
||||
members.getChallengeMembers(challengeId);
|
||||
$httpBackend.flush();
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource',
|
||||
function($scope, $rootScope, User, Notification, ApiUrl, $resource) {
|
||||
var Hero = $resource(ApiUrl.get() + '/api/v3/hall/heroes/:uid', {uid:'@_id'});
|
||||
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall',
|
||||
function($scope, $rootScope, User, Notification, ApiUrl, Hall) {
|
||||
$scope.hero = undefined;
|
||||
$scope.loadHero = function(uuid){
|
||||
Hero.query({uid:uuid}, function (heroData) {
|
||||
$scope.hero = heroData.data;
|
||||
$scope.currentHeroIndex = undefined;
|
||||
$scope.heroes = [];
|
||||
|
||||
Hall.getHeroes()
|
||||
.then(function (response) {
|
||||
$scope.heroes = response.data.data;
|
||||
});
|
||||
|
||||
$scope.loadHero = function(uuid, heroIndex) {
|
||||
$scope.currentHeroIndex = heroIndex;
|
||||
Hall.getHero(uuid)
|
||||
.then(function (response) {
|
||||
$scope.hero = response.data.data;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.saveHero = function(hero) {
|
||||
$scope.hero.contributor.admin = ($scope.hero.contributor.level > 7) ? true : false;
|
||||
hero.$save(function(){
|
||||
Notification.text("User updated");
|
||||
$scope.hero = undefined;
|
||||
$scope._heroID = undefined;
|
||||
Hero.query({}, function (heroesData) {
|
||||
$scope.heroes = heroesData.data;
|
||||
Hall.updateHero($scope.hero)
|
||||
.then(function (response) {
|
||||
Notification.text("User updated");
|
||||
$scope.hero = undefined;
|
||||
$scope._heroID = undefined;
|
||||
$scope.heroes[$scope.currentHeroIndex] = response.data.data;
|
||||
$scope.currentHeroIndex = undefined;
|
||||
});
|
||||
})
|
||||
}
|
||||
Hero.query({}, function (heroesData) {
|
||||
$scope.heroes = heroesData.data;
|
||||
});
|
||||
|
||||
$scope.populateContributorInput = function(id) {
|
||||
$scope.populateContributorInput = function(id, index) {
|
||||
$scope._heroID = id;
|
||||
window.scrollTo(0,200);
|
||||
$scope.loadHero(id);
|
||||
window.scrollTo(0, 200);
|
||||
$scope.loadHero(id, index);
|
||||
};
|
||||
}]);
|
||||
|
||||
habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', '$resource',
|
||||
function($scope, $rootScope, User, Notification, ApiUrl, $resource) {
|
||||
var Patron = $resource(ApiUrl.get() + '/api/v3/hall/patrons/:uid', {uid:'@_id'});
|
||||
|
||||
habitrpg.controller("HallPatronsCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall',
|
||||
function($scope, $rootScope, User, Notification, ApiUrl, Hall) {
|
||||
var page = 0;
|
||||
$scope.patrons = [];
|
||||
|
||||
$scope.loadMore = function(){
|
||||
Patron.query({page: page++}, function(patronsData){
|
||||
$scope.patrons = $scope.patrons.concat(patronsData.data);
|
||||
})
|
||||
$scope.loadMore = function() {
|
||||
Hall.getPatrons(page++)
|
||||
.then(function (response) {
|
||||
$scope.patrons = $scope.patrons.concat(response.data.data);
|
||||
});
|
||||
}
|
||||
$scope.loadMore();
|
||||
|
||||
|
||||
@@ -193,24 +193,27 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
||||
// Don't allow creation of an empty checklist item
|
||||
// TODO Provide UI feedback that this item is still blank
|
||||
} else if ($index == task.checklist.length - 1) {
|
||||
Tasks.addChecklistItem(task._id, task.checklist[$index]);
|
||||
task.checklist.push({completed:false,text:''});
|
||||
focusChecklist(task,task.checklist.length-1);
|
||||
Tasks.addChecklistItem(task._id, task.checklist[$index])
|
||||
.then(function (response) {
|
||||
task.checklist[$index] = response.data.data.checklist[$index];
|
||||
});
|
||||
task.checklist.push({completed:false, text:''});
|
||||
focusChecklist(task, task.checklist.length - 1);
|
||||
} else {
|
||||
$scope.saveTask(task, true);
|
||||
focusChecklist(task, $index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.removeChecklistItem = function(task, $event, $index, force){
|
||||
$scope.removeChecklistItem = function(task, $event, $index, force) {
|
||||
// Remove item if clicked on trash icon
|
||||
if (force) {
|
||||
Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
|
||||
if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
|
||||
task.checklist.splice($index, 1);
|
||||
} else if (!task.checklist[$index].text) {
|
||||
// User deleted all the text and is now wishing to delete the item
|
||||
// saveTask will prune the empty item
|
||||
Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
|
||||
if (task.checklist[$index].id) Tasks.removeChecklistItem(task._id, task.checklist[$index].id);
|
||||
// Move focus if the list is still non-empty
|
||||
if ($index > 0)
|
||||
focusChecklist(task, $index-1);
|
||||
@@ -256,6 +259,11 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
||||
User.buy({params:{key:item.key}});
|
||||
};
|
||||
|
||||
$scope.buyArmoire = function () {
|
||||
playRewardSound($scope.armoire);
|
||||
User.buyArmoire();
|
||||
}
|
||||
|
||||
/*
|
||||
------------------------
|
||||
Hiding Tasks
|
||||
|
||||
41
website/client/js/services/hallServices.js
Normal file
41
website/client/js/services/hallServices.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.factory('Hall', [ '$rootScope', 'ApiUrl', '$http',
|
||||
function($rootScope, ApiUrl, $http) {
|
||||
var apiV3Prefix = '/api/v3';
|
||||
var Hall = {};
|
||||
|
||||
Hall.getHeroes = function () {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: apiV3Prefix + '/hall/heroes',
|
||||
});
|
||||
}
|
||||
|
||||
Hall.getHero = function (uuid) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: apiV3Prefix + '/hall/heroes/' + uuid,
|
||||
});
|
||||
}
|
||||
|
||||
Hall.updateHero = function (heroDetails) {
|
||||
return $http({
|
||||
method: 'PUT',
|
||||
url: apiV3Prefix + '/hall/heroes/' + heroDetails._id,
|
||||
data: heroDetails,
|
||||
});
|
||||
}
|
||||
|
||||
Hall.getPatrons = function (page) {
|
||||
if (!page) page = 0;
|
||||
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: apiV3Prefix + '/hall/patrons?page=' + page,
|
||||
});
|
||||
}
|
||||
|
||||
return Hall;
|
||||
}]);
|
||||
@@ -38,7 +38,7 @@ angular.module('habitrpg')
|
||||
function getChallengeMembers (challengeId) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: apiV3Prefix + '/challenges/' + challengeId + '/members',
|
||||
url: apiV3Prefix + '/challenges/' + challengeId + '/members?includeAllMembers=true',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt'];
|
||||
var TASK_KEYS_TO_REMOVE = ['_id', 'completed', 'date', 'dateCompleted', 'history', 'id', 'streak', 'createdAt', 'challenge'];
|
||||
|
||||
angular.module('habitrpg')
|
||||
.factory('Tasks', ['$rootScope', 'Shared', '$http',
|
||||
|
||||
@@ -112,7 +112,6 @@ angular.module('habitrpg')
|
||||
$rootScope.$emit('userSynced');
|
||||
});
|
||||
}
|
||||
sync();
|
||||
|
||||
var save = function () {
|
||||
localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user));
|
||||
@@ -355,6 +354,17 @@ angular.module('habitrpg')
|
||||
callOpsFunctionAndRequest('buy', 'buy', "POST", data.params.key, data);
|
||||
},
|
||||
|
||||
buyArmoire: function () {
|
||||
$http({
|
||||
method: "POST",
|
||||
url: '/api/v3/user/buy-armoire',
|
||||
})
|
||||
.then(function (response) {
|
||||
Notification.text(response.data.message);
|
||||
sync();
|
||||
})
|
||||
},
|
||||
|
||||
buyQuest: function (data) {
|
||||
callOpsFunctionAndRequest('buyQuest', 'buy-quest', "POST", data.params.key, data);
|
||||
},
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"js/services/socialServices.js",
|
||||
"js/services/statServices.js",
|
||||
"js/services/userServices.js",
|
||||
"js/services/hallServices.js",
|
||||
|
||||
"js/filters/money.js",
|
||||
"js/filters/roundLargeNumbers.js",
|
||||
|
||||
@@ -352,18 +352,27 @@ api.updatePassword = {
|
||||
|
||||
if (!user.auth.local.hashed_password) throw new BadRequest(res.t('userHasNoLocalRegistration'));
|
||||
|
||||
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt);
|
||||
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword'));
|
||||
|
||||
req.checkBody({
|
||||
password: {
|
||||
notEmpty: {errorMessage: res.t('missingNewPassword')},
|
||||
},
|
||||
newPassword: {
|
||||
notEmpty: {errorMessage: res.t('missingPassword')},
|
||||
},
|
||||
newPassword: {
|
||||
notEmpty: {errorMessage: res.t('missingNewPassword')},
|
||||
},
|
||||
confirmPassword: {
|
||||
notEmpty: {errorMessage: res.t('missingNewPassword')},
|
||||
},
|
||||
});
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
|
||||
if (validationErrors) {
|
||||
throw validationErrors;
|
||||
}
|
||||
|
||||
let oldPassword = passwordUtils.encrypt(req.body.password, user.auth.local.salt);
|
||||
if (oldPassword !== user.auth.local.hashed_password) throw new NotAuthorized(res.t('wrongPassword'));
|
||||
|
||||
if (req.body.newPassword !== req.body.confirmPassword) throw new NotAuthorized(res.t('passwordConfirmationMatch'));
|
||||
|
||||
user.auth.local.hashed_password = passwordUtils.encrypt(req.body.newPassword, user.auth.local.salt); // eslint-disable-line camelcase
|
||||
|
||||
@@ -88,21 +88,20 @@ api.setCron = {
|
||||
*
|
||||
* @apiSuccess {Object} data An empty Object
|
||||
*/
|
||||
// TODO: Re-enable after v3 prod testing is done
|
||||
// api.makeAdmin = {
|
||||
// method: 'POST',
|
||||
// url: '/debug/make-admin',
|
||||
// middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
// async handler (req, res) {
|
||||
// let user = res.locals.user;
|
||||
//
|
||||
// user.contributor.admin = true;
|
||||
//
|
||||
// await user.save();
|
||||
//
|
||||
// res.respond(200, {});
|
||||
// },
|
||||
// };
|
||||
api.makeAdmin = {
|
||||
method: 'POST',
|
||||
url: '/debug/make-admin',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
user.contributor.admin = true;
|
||||
|
||||
await user.save();
|
||||
|
||||
res.respond(200, {});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/debug/modify-inventory Manipulate user's inventory
|
||||
|
||||
@@ -117,6 +117,9 @@ api.getGroups = {
|
||||
api.getGroup = {
|
||||
method: 'GET',
|
||||
url: '/groups/:groupId',
|
||||
// Disable cron when getting groups to avoid race conditions when the site is loaded
|
||||
// and requests for party and user data are concurrent
|
||||
runCron: false,
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
@@ -85,7 +85,13 @@ function _getMembersForItem (type) {
|
||||
|
||||
// optionalMembership is set to true because even if you're not member of the group you may be able to access the challenge
|
||||
// for example if you've been booted from it, are the leader or a site admin
|
||||
group = await Group.getGroup({user, groupId: challenge.group, fields: '_id type privacy', optionalMembership: true});
|
||||
group = await Group.getGroup({
|
||||
user,
|
||||
groupId: challenge.group,
|
||||
fields: '_id type privacy',
|
||||
optionalMembership: true,
|
||||
});
|
||||
|
||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||
} else {
|
||||
group = await Group.getGroup({user, groupId, fields: '_id type'});
|
||||
@@ -117,10 +123,17 @@ function _getMembersForItem (type) {
|
||||
|
||||
if (lastId) query._id = {$gt: lastId};
|
||||
|
||||
let limit = 30;
|
||||
|
||||
// Allow for all challenges members to be returned
|
||||
if (type === 'challenge-members' && req.query.includeAllMembers === 'true') {
|
||||
limit = 0; // no limit
|
||||
}
|
||||
|
||||
let members = await User
|
||||
.find(query)
|
||||
.sort({_id: 1})
|
||||
.limit(30)
|
||||
.limit(limit)
|
||||
.select(fields)
|
||||
.exec();
|
||||
|
||||
@@ -170,14 +183,19 @@ api.getInvitesForGroup = {
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/challenges/:challengeId/members Get members for a challenge
|
||||
* @apiDescription With a limit of 30 member per request. To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
|
||||
* @apiDescription With a limit of 30 member per request.
|
||||
* To get all members run requests against this routes (updating the lastId query parameter) until you get less than 30 results.
|
||||
* BETA You can also use ?includeAllMembers=true. This option is currently in BETA and may be removed in future.
|
||||
* Its use is discouraged and its performaces are not optimized especially for large challenges.
|
||||
*
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetMembersForChallenge
|
||||
* @apiGroup Member
|
||||
*
|
||||
* @apiParam {UUID} challengeId The challenge id
|
||||
* @apiParam {UUID} lastId Query parameter to specify the last member returned in a previous request to this route and get the next batch of results
|
||||
*
|
||||
* @apiParam {string} includeAllMembers BETA Query parameter - If 'true' all challenge members are returned
|
||||
|
||||
* @apiSuccess {array} data An array of members, sorted by _id
|
||||
*/
|
||||
api.getMembersForChallenge = {
|
||||
|
||||
@@ -357,7 +357,7 @@ function _generateWebhookTaskData (task, direction, delta, stats, user) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {put} /api/v3/tasks/:taskId/score/:direction Score a task
|
||||
* @api {post} /api/v3/tasks/:taskId/score/:direction Score a task
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName ScoreTask
|
||||
* @apiGroup Task
|
||||
@@ -422,13 +422,15 @@ api.scoreTask = {
|
||||
|
||||
sendTaskWebhook(user.preferences.webhooks, _generateWebhookTaskData(task, direction, delta, userStats, user));
|
||||
|
||||
if (task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
|
||||
if (task.challenge && task.challenge.id && task.challenge.taskId && !task.challenge.broken && task.type !== 'reward') {
|
||||
// Wrapping everything in a try/catch block because if an error occurs using `await` it MUST NOT bubble up because the request has already been handled
|
||||
try {
|
||||
let chalTask = await Tasks.Task.findOne({
|
||||
_id: task.challenge.taskId,
|
||||
}).exec();
|
||||
|
||||
if (!chalTask) return;
|
||||
|
||||
await chalTask.scoreChallengeTask(delta);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
|
||||
@@ -48,6 +48,15 @@ _.each(staticPages, (name) => {
|
||||
};
|
||||
});
|
||||
|
||||
api.redirectApi = {
|
||||
method: 'GET',
|
||||
url: '/static/api',
|
||||
runCron: false,
|
||||
async handler (req, res) {
|
||||
res.redirect(301, '/apidoc');
|
||||
},
|
||||
};
|
||||
|
||||
let shareables = ['level-up', 'hatch-pet', 'raise-pet', 'unlock-quest', 'won-challenge', 'achievement'];
|
||||
|
||||
_.each(shareables, (name) => {
|
||||
|
||||
@@ -154,7 +154,7 @@ api.subscribeCancel = {
|
||||
let user = res.locals.user;
|
||||
if (!user.purchased.plan.customerId) throw new NotAuthorized(res.t('missingSubscription'));
|
||||
|
||||
let customer = await stripe.customers.retrieve(user.purchased.plan.customeerId);
|
||||
let customer = await stripe.customers.retrieve(user.purchased.plan.customerId);
|
||||
await stripe.customers.del(user.purchased.plan.customerId);
|
||||
await payments.cancelSubscription({
|
||||
user,
|
||||
|
||||
@@ -5,6 +5,7 @@ import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||
const shouldDo = common.shouldDo;
|
||||
const scoreTask = common.ops.scoreTask;
|
||||
// const maxPMs = 200;
|
||||
@@ -175,13 +176,15 @@ export function cron (options = {}) {
|
||||
cron: true,
|
||||
});
|
||||
|
||||
// Apply damage from a boss, less damage for Trivial priority (difficulty)
|
||||
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
|
||||
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
|
||||
// initially, and when we realised, we could not fix it because users are used to
|
||||
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
|
||||
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
|
||||
// setting between Trivial and Easy.
|
||||
if (!CRON_SEMI_SAFE_MODE) {
|
||||
// Apply damage from a boss, less damage for Trivial priority (difficulty)
|
||||
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
|
||||
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
|
||||
// initially, and when we realised, we could not fix it because users are used to
|
||||
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
|
||||
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
|
||||
// setting between Trivial and Easy.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,20 +5,19 @@ import _ from 'lodash';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const IS_TEST = nconf.get('IS_TEST');
|
||||
const ENABLE_CONSOLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true';
|
||||
const ENABLE_LOGS_IN_TEST = nconf.get('ENABLE_CONSOLE_LOGS_IN_TEST') === 'true';
|
||||
const ENABLE_LOGS_IN_PROD = nconf.get('ENABLE_CONSOLE_LOGS_IN_PROD') === 'true';
|
||||
|
||||
const logger = new winston.Logger();
|
||||
|
||||
if (IS_PROD) {
|
||||
if (ENABLE_CONSOLE_LOGS_IN_PROD) {
|
||||
if (ENABLE_LOGS_IN_PROD) {
|
||||
logger.add(winston.transports.Console, {
|
||||
colorize: true,
|
||||
prettyPrint: true,
|
||||
colorize: false,
|
||||
prettyPrint: false,
|
||||
});
|
||||
}
|
||||
} else if (IS_TEST) {
|
||||
// Do not log anything when testing
|
||||
} else {
|
||||
} else if (!IS_TEST || IS_TEST && ENABLE_LOGS_IN_TEST) { // Do not log anything when testing unless specified
|
||||
logger
|
||||
.add(winston.transports.Console, {
|
||||
colorize: true,
|
||||
|
||||
@@ -4,10 +4,9 @@ import {
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from './email';
|
||||
import members from '../../controllers/api-v3/members';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import pushNotify from './pushNotifications';
|
||||
import sendPushNotification from './pushNotifications';
|
||||
import shared from '../../../../common' ;
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
@@ -91,7 +90,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
data.user.purchased.txnCount++;
|
||||
|
||||
if (data.gift) {
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
data.user.sendMessage(data.user, data.gift.member, data.gift);
|
||||
|
||||
let byUserName = getUserInfo(data.user, ['name']).name;
|
||||
|
||||
@@ -103,7 +102,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
}
|
||||
|
||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
|
||||
sendPushNotification(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +163,7 @@ api.buyGems = async function buyGems (data) {
|
||||
let byUsername = getUserInfo(data.user, ['name']).name;
|
||||
let gemAmount = data.gift.gems.amount || 20;
|
||||
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
data.user.sendMessage(data.user, data.gift.member, data.gift);
|
||||
if (data.gift.member.preferences.emailNotifications.giftedGems !== false) {
|
||||
txnEmail(data.gift.member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
@@ -173,7 +172,7 @@ api.buyGems = async function buyGems (data) {
|
||||
}
|
||||
|
||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
||||
sendPushNotification(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
||||
}
|
||||
|
||||
await data.gift.member.save();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import pushNotify from 'push-notify';
|
||||
@@ -26,9 +28,12 @@ if (gcm) {
|
||||
}
|
||||
|
||||
module.exports = function sendNotification (user, title, message, timeToLive = 15) {
|
||||
if (!user) return;
|
||||
return; // TODO push notifications are not currently enabled
|
||||
|
||||
_.each(user.pushDevices, pushDevice => {
|
||||
if (!user) return;
|
||||
let pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
|
||||
|
||||
_.each(pushDevices, pushDevice => {
|
||||
switch (pushDevice.type) {
|
||||
case 'android':
|
||||
if (gcm) {
|
||||
|
||||
@@ -231,7 +231,7 @@ schema.methods.removeTask = async function challengeRemoveTask (task) {
|
||||
}, {multi: true}).exec();
|
||||
};
|
||||
|
||||
// Unlink challenges tasks (and the challenge itself) from user
|
||||
// Unlink challenges tasks (and the challenge itself) from user. TODO rename to 'leave'
|
||||
schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) {
|
||||
let challengeId = this._id;
|
||||
let findQuery = {
|
||||
|
||||
@@ -517,7 +517,8 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
||||
group.quest.progress.hp -= progress.up;
|
||||
// TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests!
|
||||
let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`;
|
||||
let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} did not attack the party because it was asleep while maintenance was happening.` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
|
||||
let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' || nconf.get('CRON_SEMI_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
|
||||
// TODO Consider putting the safe mode boss attack message in an ENV var
|
||||
group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
|
||||
|
||||
// If boss has Rage, increment Rage as well
|
||||
|
||||
@@ -10,7 +10,10 @@ let Schema = mongoose.Schema;
|
||||
let discriminatorOptions = {
|
||||
discriminatorKey: 'type', // the key that distinguishes task types
|
||||
};
|
||||
let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {_id: false});
|
||||
let subDiscriminatorOptions = _.defaults(_.cloneDeep(discriminatorOptions), {
|
||||
_id: false,
|
||||
minimize: false,
|
||||
});
|
||||
|
||||
export let tasksTypes = ['habit', 'daily', 'todo', 'reward'];
|
||||
|
||||
@@ -52,7 +55,7 @@ export let TaskSchema = new Schema({
|
||||
time: {type: Date, required: true},
|
||||
}],
|
||||
}, _.defaults({
|
||||
minimize: true, // So empty objects are returned
|
||||
minimize: false, // So empty objects are returned
|
||||
strict: true,
|
||||
}, discriminatorOptions));
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
var nconf = require('nconf');
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
var _ = require('lodash');
|
||||
var locals = require('../middlewares/api-v2/locals');
|
||||
var i18n = require('../libs/api-v2/i18n');
|
||||
var md = require('markdown-it')({
|
||||
html: true,
|
||||
});
|
||||
|
||||
const TOTAL_USER_COUNT = '1,100,000';
|
||||
|
||||
// -------- App --------
|
||||
router.get('/', i18n.getUserLanguage, locals, function(req, res) {
|
||||
if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
|
||||
return res.redirect('/static/front');
|
||||
|
||||
return res.render('index', {
|
||||
title: 'Habitica | Your Life The Role Playing Game',
|
||||
env: res.locals.habitrpg
|
||||
});
|
||||
});
|
||||
|
||||
// -------- Static Pages --------
|
||||
|
||||
var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit', 'faq', 'overview', 'apps', 'clear-browser-data', 'merch', 'maintenance-info'];
|
||||
|
||||
_.each(pages, function(name){
|
||||
router.get('/static/' + name, i18n.getUserLanguage, locals, function(req, res) {
|
||||
res.render( 'static/' + name, {
|
||||
env: res.locals.habitrpg,
|
||||
md: md,
|
||||
userCount: TOTAL_USER_COUNT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// -------- Social Media Sharing --------
|
||||
|
||||
var shareables = ['level-up','hatch-pet','raise-pet','unlock-quest','won-challenge','achievement'];
|
||||
|
||||
_.each(shareables, function(name){
|
||||
router.get('/social/' + name, i18n.getUserLanguage, locals, function(req, res) {
|
||||
res.render( 'social/' + name, {
|
||||
env: res.locals.habitrpg,
|
||||
md: md,
|
||||
userCount: TOTAL_USER_COUNT
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --------- Redirects --------
|
||||
|
||||
router.get('/static/extensions', function(req, res) {
|
||||
res.redirect('http://habitica.wikia.com/wiki/App_and_Extension_Integrations');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -183,11 +183,11 @@ script(type='text/ng-template', id='partials/options.settings.settings.html')
|
||||
h5=env.t('changePass')
|
||||
form(ng-submit='changeUser("password", passwordUpdates)', ng-show='user.auth.local', name='changePassword', novalidate)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.oldPassword', required)
|
||||
input.form-control(type='password', placeholder=env.t('oldPass'), ng-model='passwordUpdates.password', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('newPass'), ng-model='passwordUpdates.newPassword', required)
|
||||
.form-group
|
||||
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmNewPassword', required)
|
||||
input.form-control(type='password', placeholder=env.t('confirmPass'), ng-model='passwordUpdates.confirmPassword', required)
|
||||
input.btn.btn-default(type='submit', ng-disabled='changePassword.$invalid', value=env.t('submit'))
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html')
|
||||
span(ng-class='userAdminGlyphiconStyle(hero)')
|
||||
span(ng-if='!hero.contributor.admin')
|
||||
a.label.label-default(ng-class='userLevelStyle(hero)', ng-click='clickMember(hero._id, true)') {{hero.profile.name}}
|
||||
td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id)').btn-link {{hero._id}}
|
||||
td(ng-if='user.contributor.admin', ng-click='populateContributorInput(hero._id, $index)').btn-link {{hero._id}}
|
||||
td {{hero.contributor.level}}
|
||||
td {{hero.contributor.text}}
|
||||
td
|
||||
|
||||
@@ -98,8 +98,7 @@ footer.footer(ng-controller='FooterCtrl')
|
||||
a.btn.btn-default(ng-click='addLevelsAndGold()') +Exp +GP +MP
|
||||
a.btn.btn-default(ng-click='addOneLevel()') +1 Level
|
||||
a.btn.btn-default(ng-click='addQuestProgress()' tooltip="+1000 to boss quests. 300 items to collection quests") Quest Progress Up
|
||||
// TODO Re-enable after v3 prod testing
|
||||
// a.btn.btn-default(ng-click='makeAdmin()') Make Admin
|
||||
a.btn.btn-default(ng-click='makeAdmin()') Make Admin
|
||||
a.btn.btn-default(ng-click='openModifyInventoryModal()') Modify Inventory
|
||||
|
||||
div(ng-init='deferredScripts()')
|
||||
|
||||
@@ -3,7 +3,7 @@ div(ng-if='task._editing')
|
||||
|
||||
// Broken Challenge
|
||||
.well(ng-if='task.challenge.broken')
|
||||
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND')
|
||||
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND"')
|
||||
p=env.t('brokenTask')
|
||||
p
|
||||
a(ng-click='unlink(task, "keep")')=env.t('keepIt')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
mixin reward(item)
|
||||
mixin reward(item, action)
|
||||
- action = action || "buy(" + item + ")"
|
||||
|
||||
li.task.reward-item(popover-trigger='mouseenter', popover-placement='top', popover='{{::#{item}.notes()}}')&attributes(attributes)
|
||||
// right-hand side control buttons
|
||||
.task-meta-controls
|
||||
@@ -8,8 +10,8 @@ mixin reward(item)
|
||||
.task-controls.task-primary
|
||||
input.reward.visuallyhidden(
|
||||
type='checkbox',
|
||||
ui-keypress='{13:"buy(#{item})"}')
|
||||
a.money.btn-buy.item-btn(ng-class='::{highValue: #{item}.value >= 1000}', ng-click='::buy(#{item})')
|
||||
ui-keypress='{13:"#{action}"}')
|
||||
a.money.btn-buy.item-btn(ng-class='::{highValue: #{item}.value >= 1000}', ng-click='::#{action}')
|
||||
span.shop_gold
|
||||
span.reward-cost {{::#{item}.value}}
|
||||
// main content
|
||||
@@ -19,5 +21,5 @@ mixin reward(item)
|
||||
ul.items.rewards(ng-if='main && list.type=="reward"')
|
||||
+reward('item')(ng-repeat='item in itemStore')
|
||||
+reward('healthPotion')
|
||||
+reward('armoire')(ng-if='user.flags.armoireEnabled',
|
||||
+reward('armoire', 'buyArmoire()')(ng-if='user.flags.armoireEnabled',
|
||||
popover='{{armoire.notes(user, armoireCount(user.items.gear.owned))}}')
|
||||
|
||||
Reference in New Issue
Block a user