mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-04-25 03:44:03 -05:00
Merge branch 'develop' into release
This commit is contained in:
+8
-8
@@ -16,18 +16,18 @@ before_script:
|
||||
- npm run test:build
|
||||
- cp config.json.example config.json
|
||||
- if [ $REQUIRES_SERVER ]; then until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done; export DISPLAY=:99; fi
|
||||
after_script:
|
||||
- ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js
|
||||
script: npm run $TEST
|
||||
script:
|
||||
- npm run $TEST
|
||||
- if [ $COVERAGE ]; then ./node_modules/.bin/lcov-result-merger 'coverage/**/*.info' | ./node_modules/coveralls/bin/coveralls.js; fi
|
||||
env:
|
||||
global:
|
||||
- CXX=g++-4.8
|
||||
- DISABLE_REQUEST_LOGGING=true
|
||||
matrix:
|
||||
- TEST="lint"
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true
|
||||
- TEST="test:api-v3" REQUIRES_SERVER=true COVERAGE=true
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content"
|
||||
- TEST="test:common"
|
||||
- TEST="test:karma"
|
||||
- TEST="client:unit"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="test:karma" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
|
||||
+2
-2
@@ -280,7 +280,7 @@ gulp.task('test:e2e:safe', ['test:prepare', 'test:prepare:server'], (cb) => {
|
||||
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
@@ -298,7 +298,7 @@ gulp.task('test:api-v3:unit:watch', () => {
|
||||
|
||||
gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
|
||||
Generated
+656
-470
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -139,9 +139,9 @@
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
"test:api-v3:integration": "gulp test:api-v3:integration",
|
||||
"test:api-v3:integration:separate-server": "NODE_ENV=test gulp test:api-v3:integration:separate-server",
|
||||
"test:sanity": "mocha test/sanity --recursive",
|
||||
"test:common": "mocha test/common --recursive",
|
||||
"test:content": "mocha test/content --recursive",
|
||||
"test:sanity": "istanbul cover --dir coverage/sanity --report lcovonly node_modules/mocha/bin/_mocha -- test/sanity --recursive",
|
||||
"test:common": "istanbul cover --dir coverage/common --report lcovonly node_modules/mocha/bin/_mocha -- test/common --recursive",
|
||||
"test:content": "istanbul cover --dir coverage/content --report lcovonly node_modules/mocha/bin/_mocha -- test/content --recursive",
|
||||
"test:karma": "karma start test/client-old/spec/karma.conf.js --single-run",
|
||||
"test:karma:watch": "karma start test/client-old/spec/karma.conf.js",
|
||||
"test:prepare:webdriver": "webdriver-manager update",
|
||||
@@ -183,7 +183,7 @@
|
||||
"grunt-karma": "~0.12.1",
|
||||
"http-proxy-middleware": "^0.17.0",
|
||||
"inject-loader": "^3.0.0-beta4",
|
||||
"istanbul": "^0.3.14",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^1.3.0",
|
||||
"karma-babel-preprocessor": "^6.0.1",
|
||||
"karma-chai-plugins": "~0.6.0",
|
||||
@@ -198,7 +198,7 @@
|
||||
"karma-webpack": "^2.0.2",
|
||||
"lcov-result-merger": "^1.0.2",
|
||||
"lolex": "^1.4.0",
|
||||
"mocha": "^2.3.3",
|
||||
"mocha": "^3.2.0",
|
||||
"mongodb": "^2.0.46",
|
||||
"mongoskin": "~2.1.0",
|
||||
"monk": "^4.0.0",
|
||||
|
||||
@@ -63,15 +63,17 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
});
|
||||
|
||||
it('returns only first 30 invites', async () => {
|
||||
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
let leader = await generateUser({balance: 4});
|
||||
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||
|
||||
let invitesToGenerate = [];
|
||||
for (let i = 0; i < 31; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||
await leader.post(`/groups/${group._id}/invite`, {uuids: generatedInvites.map(invite => invite._id)});
|
||||
|
||||
let res = await user.get('/groups/party/invites');
|
||||
let res = await leader.get(`/groups/${group._id}/invites`);
|
||||
expect(res.length).to.equal(30);
|
||||
res.forEach(member => {
|
||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
const INVITES_LIMIT = 100;
|
||||
const PARTY_LIMIT_MEMBERS = 30;
|
||||
|
||||
describe('Post /groups/:groupId/invite', () => {
|
||||
let inviter;
|
||||
@@ -321,6 +322,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows 30+ members in a guild', async () => {
|
||||
let invitesToGenerate = [];
|
||||
// Generate 30 users to invite (30 + leader = 31 members)
|
||||
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
// Invite users
|
||||
expect(await inviter.post(`/groups/${group._id}/invite`, {
|
||||
uuids: generatedInvites.map(invite => invite._id),
|
||||
})).to.be.an('array');
|
||||
});
|
||||
|
||||
// @TODO: Add this after we are able to mock the group plan route
|
||||
xit('returns an error when a non-leader invites to a group plan', async () => {
|
||||
let userToInvite = await generateUser();
|
||||
@@ -410,5 +424,36 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
expect((await userToInvite.get('/user')).invitations.party.id).to.equal(party._id);
|
||||
});
|
||||
|
||||
it('allows 30 members in a party', async () => {
|
||||
let invitesToGenerate = [];
|
||||
// Generate 29 users to invite (29 + leader = 30 members)
|
||||
for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
// Invite users
|
||||
expect(await inviter.post(`/groups/${party._id}/invite`, {
|
||||
uuids: generatedInvites.map(invite => invite._id),
|
||||
})).to.be.an('array');
|
||||
});
|
||||
|
||||
it('does not allow 30+ members in a party', async () => {
|
||||
let invitesToGenerate = [];
|
||||
// Generate 30 users to invite (30 + leader = 31 members)
|
||||
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i++) {
|
||||
invitesToGenerate.push(generateUser());
|
||||
}
|
||||
let generatedInvites = await Promise.all(invitesToGenerate);
|
||||
// Invite users
|
||||
await expect(inviter.post(`/groups/${party._id}/invite`, {
|
||||
uuids: generatedInvites.map(invite => invite._id),
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('partyExceedsMembersLimit', {maxMembersParty: PARTY_LIMIT_MEMBERS}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,6 @@ import validator from 'validator';
|
||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../../../website/server/libs/errors';
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
@@ -460,73 +457,67 @@ describe('Group Model', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('throws an error if no uuids or emails are passed in', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
||||
done();
|
||||
}
|
||||
it('throws an error if no uuids or emails are passed in', async () => {
|
||||
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteEmailUuid');
|
||||
});
|
||||
|
||||
it('throws an error if only uuids are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations({ uuid: 'user-id'}, null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
it('throws an error if only uuids are passed in, but they are not an array', async () => {
|
||||
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('uuidsMustBeAnArray');
|
||||
});
|
||||
|
||||
it('throws an error if only emails are passed in, but they are not an array', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, { emails: 'user@example.com'}, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
||||
done();
|
||||
}
|
||||
it('throws an error if only emails are passed in, but they are not an array', async () => {
|
||||
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('emailsMustBeAnArray');
|
||||
});
|
||||
|
||||
it('throws an error if emails are not passed in, and uuid array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], null, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
||||
done();
|
||||
}
|
||||
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
|
||||
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
||||
});
|
||||
|
||||
it('throws an error if uuids are not passed in, and email array is empty', (done) => {
|
||||
try {
|
||||
Group.validateInvitations(null, [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
||||
done();
|
||||
}
|
||||
it('throws an error if uuids are not passed in, and email array is empty', async () => {
|
||||
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
||||
});
|
||||
|
||||
it('throws an error if uuids and emails are passed in as empty arrays', (done) => {
|
||||
try {
|
||||
Group.validateInvitations([], [], res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||
done();
|
||||
}
|
||||
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
|
||||
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||
});
|
||||
|
||||
it('throws an error if total invites exceed max invite constant', (done) => {
|
||||
it('throws an error if total invites exceed max invite constant', async () => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
@@ -537,17 +528,16 @@ describe('Group Model', () => {
|
||||
|
||||
uuids.push('one-more-uuid'); // to put it over the limit
|
||||
|
||||
try {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
||||
done();
|
||||
}
|
||||
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
|
||||
httpCode: 400,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
expect(res.t).to.be.calledOnce;
|
||||
expect(res.t).to.be.calledWith('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT });
|
||||
});
|
||||
|
||||
it('does not throw error if number of invites matches max invite limit', () => {
|
||||
it('does not throw error if number of invites matches max invite limit', async () => {
|
||||
let uuids = [];
|
||||
let emails = [];
|
||||
|
||||
@@ -556,49 +546,33 @@ describe('Group Model', () => {
|
||||
emails.push(`user-${i}@example.com`);
|
||||
}
|
||||
|
||||
expect(function () {
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
}).to.not.throw();
|
||||
});
|
||||
|
||||
|
||||
it('does not throw an error if only user ids are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
||||
}).to.not.throw();
|
||||
|
||||
await Group.validateInvitations(uuids, emails, res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if only emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
it('does not throw an error if only user ids are passed in', async () => {
|
||||
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if both uuids and emails are passed in', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
|
||||
it('does not throw an error if only emails are passed in', async () => {
|
||||
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if uuids are passed in and emails are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
||||
}).to.not.throw();
|
||||
|
||||
it('does not throw an error if both uuids and emails are passed in', async () => {
|
||||
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if emails are passed in and uuids are an empty array', () => {
|
||||
expect(function () {
|
||||
Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
||||
}).to.not.throw();
|
||||
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
|
||||
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
|
||||
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
||||
expect(res.t).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
describe('task Directive', () => {
|
||||
var compile, scope, directiveElem, $modal;
|
||||
|
||||
beforeEach(function(){
|
||||
module(function($provide) {
|
||||
$modal = {
|
||||
open: sandbox.spy(),
|
||||
};
|
||||
$provide.value('$modal', $modal);
|
||||
});
|
||||
|
||||
inject(function($compile, $rootScope, $templateCache) {
|
||||
compile = $compile;
|
||||
scope = $rootScope.$new();
|
||||
|
||||
$templateCache.put('templates/task.html', '<div>Task</div>');
|
||||
});
|
||||
|
||||
|
||||
directiveElem = getCompiledElement();
|
||||
});
|
||||
|
||||
function getCompiledElement(){
|
||||
var element = angular.element('<task></task>');
|
||||
var compiledElement = compile(element)(scope);
|
||||
scope.$digest();
|
||||
return compiledElement;
|
||||
}
|
||||
|
||||
xit('opens task note modal', () => {
|
||||
scope.showNoteDetails();
|
||||
|
||||
expect($modal.open).to.be.calledOnce;
|
||||
});
|
||||
})
|
||||
@@ -81,8 +81,11 @@ module.exports = function karmaConfig (config) {
|
||||
},
|
||||
|
||||
coverageReporter: {
|
||||
type: 'lcov',
|
||||
dir: 'coverage/karma',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' },
|
||||
],
|
||||
dir: '../../../coverage/karma',
|
||||
},
|
||||
|
||||
// Enable mocha-style reporting, for better test visibility
|
||||
|
||||
@@ -28,7 +28,7 @@ module.exports = function (config) {
|
||||
noInfo: true,
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: './coverage',
|
||||
dir: '../../../coverage/client-unit',
|
||||
reporters: [
|
||||
{ type: 'lcov', subdir: '.' },
|
||||
{ type: 'text-summary' },
|
||||
|
||||
@@ -178,6 +178,8 @@
|
||||
hrpg-button-color-mixin(lighten($color-toolbar,32.8%))
|
||||
.toolbar-subscribe-button, .toolbar-controls .toolbar-subscribe-button
|
||||
@extend $hrpg-button
|
||||
button
|
||||
min-width: 130px
|
||||
@media screen and (max-width: $xs-max-screen-width)
|
||||
.toolbar-toggle
|
||||
display: none
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
|
||||
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
|
||||
$scope.PARTY_LIMIT_MEMBERS = Shared.constants.PARTY_LIMIT_MEMBERS;
|
||||
|
||||
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
|
||||
$scope.isMemberOfPendingQuest = function (userid, group) {
|
||||
if (!group.quest || !group.quest.members) return false;
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
(function(){
|
||||
angular
|
||||
.module('habitrpg')
|
||||
.directive('task', task);
|
||||
|
||||
task.$inject = [
|
||||
'Shared',
|
||||
];
|
||||
|
||||
function task(Shared) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'templates/task.html',
|
||||
scope: true,
|
||||
link: function($scope, element, attrs) {
|
||||
$scope.getClasses = function (task, user, list, main) {
|
||||
return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
'use strict';
|
||||
|
||||
(function(){
|
||||
angular
|
||||
.module('habitrpg')
|
||||
.directive('task', task);
|
||||
|
||||
task.$inject = [
|
||||
'Shared',
|
||||
'$modal',
|
||||
];
|
||||
|
||||
function task(Shared, $modal) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'templates/task.html',
|
||||
scope: true,
|
||||
link: function($scope, element, attrs) {
|
||||
$scope.getClasses = function (task, user, list, main) {
|
||||
return Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main);
|
||||
}
|
||||
|
||||
$scope.showNoteDetails = function (task) {
|
||||
task.popoverOpen = false;
|
||||
|
||||
$modal.open({
|
||||
templateUrl: 'modals/task-extra-notes.html',
|
||||
controller: ['$scope', 'task', function ($scope, task) {
|
||||
$scope.task = task;
|
||||
}],
|
||||
resolve: {
|
||||
task: function() {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
@@ -260,6 +260,7 @@ angular.module('habitrpg')
|
||||
if (quest.progressDelta && userQuest.boss) {
|
||||
Notification.quest('questDamage', quest.progressDelta.toFixed(1));
|
||||
} else if (quest.collection && userQuest.collect) {
|
||||
user.party.quest.progress.collectedItems++;
|
||||
Notification.quest('questCollection', quest.collection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
"commGuidePara039": "The Back Corner Guild is a free public space to discuss sensitive subjects, and it is carefully moderated. It is not a place for general discussions or conversations. <strong>The Public Space Guidelines still apply, as do all of the Terms and Conditions.</strong> Just because we are wearing long cloaks and clustering in a corner doesn't mean that anything goes! Now pass me that smoldering candle, will you?",
|
||||
"commGuideHeadingTrello": "Trello Boards",
|
||||
"commGuidePara040": "<strong>Trello serves as an open forum for suggestions and discussion of site features.</strong> Habitica is ruled by the people in the form of valiant contributors -- we all build the site together. Trello lends structure to our system. Out of consideration for this, <strong>try your best to contain all your thoughts into one comment, instead of commenting many times in a row on the same card. If you think of something new, feel free to edit your original comments.</strong> Please, take pity on those of us who receive a notification for every new comment. Our inboxes can only withstand so much.",
|
||||
"commGuidePara041": "Habitica uses five different Trello boards:",
|
||||
"commGuidePara041": "Habitica uses four different Trello boards:",
|
||||
"commGuideList03A": "The <strong>Main Board</strong> is a place to request and vote on site features.",
|
||||
"commGuideList03B": "The <strong>Mobile Board</strong> is a place to request and vote on mobile app features.",
|
||||
"commGuideList03C": "The <strong>Pixel Art Board</strong> is a place to discuss and submit pixel art.",
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
"leaderOnlyChallenges": "Only group leader can create challenges",
|
||||
"sendGift": "Send Gift",
|
||||
"inviteFriends": "Invite Friends",
|
||||
"partyMembersInfo": "Your party currently has <%= memberCount %> members and <%= invitationCount %> pending invitations. The limit of members in a party is <%= limitMembers %>. Invitations above this limit cannot be sent.",
|
||||
"inviteByEmail": "Invite by Email",
|
||||
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
|
||||
"inviteFriendsNow": "Invite Friends Now",
|
||||
@@ -154,6 +155,7 @@
|
||||
"sendGiftPurchase": "Purchase",
|
||||
"sendGiftMessagePlaceholder": "Personal message (optional)",
|
||||
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
|
||||
"gemGiftsAreOptional": "Please note that Habitica will never require you to gift gems to other players. Begging people for gems is a <strong>violation of the Community Guidelines</strong>, and all such instances should be reported to <%= hrefTechAssistanceEmail %>.",
|
||||
"battleWithFriends": "Battle Monsters With Friends",
|
||||
"startPartyWithFriends": "Start a Party with your friends!",
|
||||
"startAParty": "Start a Party",
|
||||
@@ -201,6 +203,7 @@
|
||||
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
||||
"emailsMustBeAnArray": "Email address invites must be an array.",
|
||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
||||
"partyExceedsMembersLimit": "Party size is limited to <%= maxMembersParty %> members",
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"confirmScoreNotes": "Confirm task scoring with notes",
|
||||
"taskScoreNotesTooLong": "Task score notes must be less than 256 characters",
|
||||
"groupTasksByChallenge": "Group tasks by challenge title",
|
||||
"taskNotes": "Task Notes",
|
||||
"monthlyRepeatHelpContent": "This task will be due every X months",
|
||||
"yearlyRepeatHelpContent": "This task will be due every X years"
|
||||
}
|
||||
|
||||
@@ -12,4 +12,6 @@ export const SUPPORTED_SOCIAL_NETWORKS = [
|
||||
{key: 'google', name: 'Google'},
|
||||
];
|
||||
|
||||
export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination
|
||||
export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when using pagination
|
||||
|
||||
export const PARTY_LIMIT_MEMBERS = 30;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
GUILDS_PER_PAGE,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
} from './constants';
|
||||
|
||||
api.constants = {
|
||||
@@ -34,6 +35,7 @@ api.constants = {
|
||||
LARGE_GROUP_COUNT_MESSAGE_CUTOFF,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
GUILDS_PER_PAGE,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
};
|
||||
// TODO Move these under api.constants
|
||||
api.maxLevel = MAX_LEVEL;
|
||||
|
||||
@@ -1038,6 +1038,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
|
||||
* @apiError (400) {BadRequest} MustBeArray The `uuids` or `emails` body param was not an array.
|
||||
* @apiError (400) {BadRequest} TooManyInvites A max of 100 invites (combined emails and user ids) can
|
||||
* be sent out at a time.
|
||||
* @apiError (400) {BadRequest} ExceedsMembersLimit A max of 30 members can join a party.
|
||||
*
|
||||
* @apiError (401) {NotAuthorized} UserAlreadyInvited The user has already been invited to the group.
|
||||
* @apiError (401) {NotAuthorized} UserAlreadyInGroup The user is already a member of the group.
|
||||
@@ -1066,7 +1067,7 @@ api.inviteToGroup = {
|
||||
let uuids = req.body.uuids;
|
||||
let emails = req.body.emails;
|
||||
|
||||
Group.validateInvitations(uuids, emails, res);
|
||||
await Group.validateInvitations(uuids, emails, res, group);
|
||||
|
||||
let results = [];
|
||||
|
||||
|
||||
@@ -27,8 +27,39 @@ let api = {};
|
||||
* @apiName UserGet
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiDescription The user profile contains data related to the authenticated user including (but not limited to);
|
||||
* Achievements
|
||||
* Authentications (including types and timestamps)
|
||||
* Challenges
|
||||
* Flags (including armoire, tutorial, tour etc...)
|
||||
* Guilds
|
||||
* History (including timestamps and values)
|
||||
* Inbox (includes message history)
|
||||
* Invitations (to parties/guilds)
|
||||
* Items (character's full inventory)
|
||||
* New Messages (flags for groups/guilds that have new messages)
|
||||
* Notifications
|
||||
* Party (includes current quest information)
|
||||
* Preferences (user selected prefs)
|
||||
* Profile (name, photo url, blurb)
|
||||
* Purchased (includes purchase history, gem purchased items, plans)
|
||||
* PushDevices (identifiers for mobile devices authorized)
|
||||
* Stats (standard RPG stats, class, buffs, xp, etc..)
|
||||
* Tags
|
||||
* TasksOrder (list of all ids for dailys, habits, rewards and todos)
|
||||
*
|
||||
* @apiSuccess {Object} data The user object
|
||||
*/
|
||||
*
|
||||
* @apiSuccessExample {json} Result:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* -- User data included here, for details of the user model see:
|
||||
* -- https://github.com/HabitRPG/habitica/tree/develop/website/server/models/user
|
||||
* }
|
||||
* }
|
||||
*
|
||||
*/
|
||||
api.getUser = {
|
||||
method: 'GET',
|
||||
middlewares: [authWithHeaders()],
|
||||
@@ -46,11 +77,30 @@ api.getUser = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/user/inventory/buy Get the gear items available for purchase for the current user
|
||||
* @api {get} /api/v3/user/inventory/buy Get the gear items available for purchase for the authenticated user
|
||||
* @apiName UserGetBuyList
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccess {Object} data The buy list
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": [
|
||||
* {
|
||||
* "text": "Training Sword",
|
||||
* "notes": "Practice weapon. Confers no benefit.",
|
||||
* "value": 1,
|
||||
* "type": "weapon",
|
||||
* "key": "weapon_warrior_0",
|
||||
* "set": "warrior-0",
|
||||
* "klass": "warrior",
|
||||
* "index": "0",
|
||||
* "str": 0,
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "con": 0
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
api.getBuyList = {
|
||||
method: 'GET',
|
||||
@@ -142,11 +192,31 @@ let checkPreferencePurchase = (user, path, item) => {
|
||||
|
||||
/**
|
||||
* @api {put} /api/v3/user Update the user
|
||||
* @apiDescription Example body: {'stats.hp':50, 'preferences.background': 'beach'}
|
||||
* @apiName UserUpdate
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccess {Object} data The updated user object
|
||||
* @apiDescription Some of the user items can be updated, such as preferences, flags and stats.
|
||||
^
|
||||
* @apiParamExample {json} Request-Example:
|
||||
* {
|
||||
* "achievements.habitBirthdays": 2,
|
||||
* "profile.name": "MadPink",
|
||||
* "stats.hp": 53,
|
||||
* "flags.warnedLowHealth":false,
|
||||
* "preferences.allocationMode":"flat",
|
||||
* "preferences.hair.bangs": 3
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data The updated user object, the result is identical to the get user call
|
||||
*
|
||||
* @apiError (401) {NotAuthorized} messageUserOperationProtected Returned if the change is not allowed.
|
||||
*
|
||||
* @apiErrorExample {json} Error-Response:
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "path `stats.class` was not saved, as it's a protected path."
|
||||
* }
|
||||
*/
|
||||
api.updateUser = {
|
||||
method: 'PUT',
|
||||
@@ -179,9 +249,32 @@ api.updateUser = {
|
||||
* @apiName UserDelete
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String} password The user's password (unless it's a Facebook account)
|
||||
* @apiParam {String} password The user's password if the account uses local authentication
|
||||
*
|
||||
* @apiSuccess {Object} data An empty Object
|
||||
*
|
||||
* @apiSuccessExample {json} Result:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {}
|
||||
* }
|
||||
*
|
||||
* @apiError {BadRequest} MissingPassword The password was not included in the request
|
||||
* @apiError {BadRequest} NotAuthorized There is no account that uses those credentials.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "BadRequest",
|
||||
* "message": "Invalid request parameters.",
|
||||
* "errors": [
|
||||
* {
|
||||
* "message": "Missing password.",
|
||||
* "param": "password"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
*/
|
||||
api.deleteUser = {
|
||||
method: 'DELETE',
|
||||
@@ -240,8 +333,17 @@ function _cleanChecklist (task) {
|
||||
* @apiName UserGetAnonymized
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiDescription Returns the user's data without:
|
||||
* Authentication information
|
||||
* NewMessages/Invitations/Inbox
|
||||
* Profile
|
||||
* Purchased information
|
||||
* Contributor information
|
||||
* Special items
|
||||
* Webhooks
|
||||
*
|
||||
* @apiSuccess {Object} data.user
|
||||
* @apiSuccess {Array} data.tasks
|
||||
* @apiSuccess {Object} data.tasks
|
||||
**/
|
||||
api.getUserAnonymized = {
|
||||
method: 'GET',
|
||||
@@ -306,11 +408,15 @@ const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String=fireball, mpHeal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast.
|
||||
* @apiParam {UUID} targetId Optional query parameter, the id of the target when casting a skill on a party member or a task
|
||||
* @apiParam (Body) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on onesself or the user's current party.
|
||||
* @apiParamExample {json} Query example:
|
||||
* {
|
||||
* "targetId":"fd427623-9a69-4aac-9852-13deb9c190c3"
|
||||
* }
|
||||
*
|
||||
* @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned.
|
||||
*
|
||||
* @apiExample Skill Key to Name Mapping
|
||||
* @apiDescription Skill Key to Name Mapping
|
||||
* Mage
|
||||
* fireball: "Burst of Flames"
|
||||
* mpHeal: "Ethereal Surge"
|
||||
@@ -335,10 +441,10 @@ const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
* brightness: "Searing Brightness"
|
||||
* healAll: "Blessing"
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} Not enough mana.
|
||||
* @apiUse TaskNotFound
|
||||
* @apiUse PartyNotFound
|
||||
* @apiUse UserNotFound
|
||||
*
|
||||
*/
|
||||
api.castSpell = {
|
||||
method: 'POST',
|
||||
@@ -501,7 +607,15 @@ api.castSpell = {
|
||||
* @apiName UserSleep
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiDescription Toggles the sleep key under user preference true and false.
|
||||
*
|
||||
* @apiSuccess {boolean} data user.preferences.sleep
|
||||
*
|
||||
* @apiSuccessExample {json} Return-example
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": false
|
||||
* }
|
||||
*/
|
||||
api.sleep = {
|
||||
method: 'POST',
|
||||
@@ -516,13 +630,25 @@ api.sleep = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate Allocate an attribute point
|
||||
* @api {post} /api/v3/user/allocate Allocate a single attribute point
|
||||
* @apiName UserAllocate
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String} stat Query parameter - Defaults to 'str', mast be one of be of str, con, int or per
|
||||
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
|
||||
*
|
||||
* @apiSuccess {Object} data user.stats
|
||||
* @apiParamExample {json} Example request
|
||||
* {"stat":"int"}
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* }
|
||||
*/
|
||||
api.allocate = {
|
||||
method: 'POST',
|
||||
@@ -538,10 +664,46 @@ api.allocate = {
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate-now Allocate all attribute points
|
||||
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR.
|
||||
* @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. Note: will return success, even if there are 0 points to allocate.
|
||||
* @apiName UserAllocateNow
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "hp": 50,
|
||||
* "mp": 38,
|
||||
* "exp": 7,
|
||||
* "gp": 284.8637271160258,
|
||||
* "lvl": 10,
|
||||
* "class": "rogue",
|
||||
* "points": 0,
|
||||
* "str": 2,
|
||||
* "con": 2,
|
||||
* "int": 3,
|
||||
* "per": 3,
|
||||
* "buffs": {
|
||||
* "str": 0,
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "con": 0,
|
||||
* "stealth": 0,
|
||||
* "streaks": false,
|
||||
* "snowball": false,
|
||||
* "spookySparkles": false,
|
||||
* "shinySeed": false,
|
||||
* "seafoam": false
|
||||
* },
|
||||
* "training": {
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "str": 0,
|
||||
* "con": 0
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data user.stats
|
||||
*/
|
||||
api.allocateNow = {
|
||||
@@ -563,6 +725,27 @@ api.allocateNow = {
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam {String} key The item to buy
|
||||
*
|
||||
* @apiSuccess data User's data profile
|
||||
* @apiSuccess message Item purchased
|
||||
*
|
||||
* @apiSuccessExample {json} Purchased a rogue short sword for example:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* ---TRUNCATED USER RECORD---
|
||||
* },
|
||||
* "message": "Bought Short Sword"
|
||||
* }
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} messageAlreadyOwnGear Already own equipment
|
||||
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
||||
*
|
||||
* @apiErrorExample {json} NotAuthorized Already own
|
||||
* {"success":false,"error":"NotAuthorized","message":"You already own that piece of equipment"}
|
||||
*
|
||||
* @apiErrorExample {json} NotAuthorized Not enough gold
|
||||
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
||||
*/
|
||||
api.buy = {
|
||||
method: 'POST',
|
||||
|
||||
@@ -300,7 +300,7 @@ schema.statics.toJSONCleanChat = function groupToJSONCleanChat (group, user) {
|
||||
* @param res Express res object for use with translations
|
||||
* @throws BadRequest An error describing the issue with the invitations
|
||||
*/
|
||||
schema.statics.validateInvitations = function getInvitationError (uuids, emails, res) {
|
||||
schema.statics.validateInvitations = async function getInvitationError (uuids, emails, res, group = null) {
|
||||
let uuidsIsArray = Array.isArray(uuids);
|
||||
let emailsIsArray = Array.isArray(emails);
|
||||
let emptyEmails = emailsIsArray && emails.length < 1;
|
||||
@@ -339,6 +339,27 @@ schema.statics.validateInvitations = function getInvitationError (uuids, emails,
|
||||
if (totalInvites > INVITES_LIMIT) {
|
||||
throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}));
|
||||
}
|
||||
|
||||
// If party, check the limit of members
|
||||
if (group && group.type === 'party') {
|
||||
let memberCount = 0;
|
||||
|
||||
// Counting the members that already joined the party
|
||||
memberCount += group.memberCount;
|
||||
|
||||
// Count how many invitations currently exist in the party
|
||||
let query = {};
|
||||
query['invitations.party.id'] = group._id;
|
||||
let groupInvites = await User.count(query).exec();
|
||||
memberCount += groupInvites;
|
||||
|
||||
// Counting the members that are going to be invited by email and uuids
|
||||
memberCount += totalInvites;
|
||||
|
||||
if (memberCount > shared.constants.PARTY_LIMIT_MEMBERS) {
|
||||
throw new BadRequest(res.t('partyExceedsMembersLimit', {maxMembersParty: shared.constants.PARTY_LIMIT_MEMBERS}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
schema.methods.getParticipatingQuestMembers = function getParticipatingQuestMembers () {
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
script(id='partials/options.profile.profile.html', type='text/ng-template')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-12(ng-show='!_editing.profile')
|
||||
button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile')= env.t('edit')
|
||||
h4=env.t('displayName')
|
||||
span(ng-show='profile.profile.name') {{profile.profile.name}}
|
||||
p
|
||||
small.muted=env.t('displayNameDescription1')
|
||||
|
|
||||
a(href='/#/options/settings/settings')=env.t('displayNameDescription2')
|
||||
|
|
||||
=env.t('displayNameDescription3')
|
||||
span.muted(ng-hide='profile.profile.name') -
|
||||
=env.t('none')
|
||||
| -
|
||||
|
||||
h4=env.t('displayPhoto')
|
||||
img.img-rendering-auto(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}')
|
||||
span.muted(ng-hide='profile.profile.imageUrl') -
|
||||
=env.t('none')
|
||||
| -
|
||||
|
||||
h4=env.t('displayBlurb')
|
||||
markdown(ng-show='profile.profile.blurb', text='profile.profile.blurb')
|
||||
span.muted(ng-hide='profile.profile.blurb') -
|
||||
=env.t('none')
|
||||
| -
|
||||
//{{profile.profile.blurb | linky:'_blank'}}
|
||||
.row
|
||||
.col-md-6
|
||||
h4=env.t('totalCheckinsTitle')
|
||||
span {{env.t('totalCheckins', {count: user.loginIncentives})}}
|
||||
.col-md-6
|
||||
h4
|
||||
| {{::getProgressDisplay()}}
|
||||
.progress
|
||||
.progress-bar(role='progressbar', aria-valuenow='{{::incentivesProgress()}}', aria-valuemin='0', aria-valuemax='100', style='width: {{::incentivesProgress()}}%;')
|
||||
span.sr-only {{::incentivesProgress()}}% Complete
|
||||
|
||||
form.col-md-4(ng-show='_editing.profile', ng-submit='save()')
|
||||
.alert.alert-info.alert-sm
|
||||
!=env.t("communityGuidelinesWarning")
|
||||
input.btn.btn-primary(type='submit', value=env.t('save'))
|
||||
// TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak
|
||||
.form-group
|
||||
label=env.t('displayName')
|
||||
input.form-control(type='text', placeholder=env.t('fullName'), ng-model='editingProfile.name')
|
||||
.form-group
|
||||
label=env.t('photoUrl')
|
||||
input.form-control(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageUrl'))
|
||||
.form-group
|
||||
label=env.t('displayBlurb')
|
||||
textarea.form-control(rows=5, placeholder=env.t('displayBlurbPlaceholder'), ng-model='editingProfile.blurb')
|
||||
include ../../shared/formatting-help
|
||||
script(id='partials/options.profile.profile.html', type='text/ng-template')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-12(ng-show='!_editing.profile')
|
||||
button.btn.btn-default(ng-click='_editing.profile = true', ng-show='!_editing.profile')= env.t('edit')
|
||||
h4=env.t('displayName')
|
||||
span(ng-show='profile.profile.name') {{profile.profile.name}}
|
||||
p
|
||||
small.muted=env.t('displayNameDescription1')
|
||||
|
|
||||
a(href='/#/options/settings/settings')=env.t('displayNameDescription2')
|
||||
|
|
||||
=env.t('displayNameDescription3')
|
||||
span.muted(ng-hide='profile.profile.name') -
|
||||
=env.t('none')
|
||||
| -
|
||||
|
||||
h4=env.t('displayPhoto')
|
||||
img.img-rendering-auto(ng-show='profile.profile.imageUrl', ng-src='{{profile.profile.imageUrl}}')
|
||||
span.muted(ng-hide='profile.profile.imageUrl') -
|
||||
=env.t('none')
|
||||
| -
|
||||
|
||||
h4=env.t('displayBlurb')
|
||||
markdown(ng-show='profile.profile.blurb', text='profile.profile.blurb')
|
||||
span.muted(ng-hide='profile.profile.blurb') -
|
||||
=env.t('none')
|
||||
| -
|
||||
//{{profile.profile.blurb | linky:'_blank'}}
|
||||
.row
|
||||
.col-md-6
|
||||
h4=env.t('totalCheckinsTitle')
|
||||
span {{env.t('totalCheckins', {count: user.loginIncentives})}}
|
||||
.col-md-6
|
||||
h4
|
||||
| {{::getProgressDisplay()}}
|
||||
.progress
|
||||
.progress-bar(role='progressbar', aria-valuenow='{{::incentivesProgress()}}', aria-valuemin='0', aria-valuemax='100', style='width: {{::incentivesProgress()}}%;')
|
||||
span.sr-only {{::incentivesProgress()}}% Complete
|
||||
|
||||
form.col-md-4(ng-show='_editing.profile', ng-submit='save()')
|
||||
.alert.alert-info.alert-sm
|
||||
!=env.t("communityGuidelinesWarning", { hrefBlankCommunityManagerEmail : '<a href="mailto:' + env.EMAILS.COMMUNITY_MANAGER_EMAIL + '">' + env.EMAILS.COMMUNITY_MANAGER_EMAIL + '</a>'})
|
||||
input.btn.btn-primary(type='submit', value=env.t('save'))
|
||||
// TODO use photo-upload instead: https://groups.google.com/forum/?fromgroups=#!topic/derbyjs/xMmADvxBOak
|
||||
.form-group
|
||||
label=env.t('displayName')
|
||||
input.form-control(type='text', placeholder=env.t('fullName'), ng-model='editingProfile.name')
|
||||
.form-group
|
||||
label=env.t('photoUrl')
|
||||
input.form-control(type='url', ng-model='editingProfile.imageUrl', placeholder=env.t('imageUrl'))
|
||||
.form-group
|
||||
label=env.t('displayBlurb')
|
||||
textarea.form-control(rows=5, placeholder=env.t('displayBlurbPlaceholder'), ng-model='editingProfile.blurb')
|
||||
include ../../shared/formatting-help
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6
|
||||
h2=env.t('API')
|
||||
small=env.t('APIText')
|
||||
h6=env.t('userId')
|
||||
pre.prettyprint {{user.id}}
|
||||
h6=env.t('APIToken')
|
||||
pre.prettyprint {{User.settings.auth.apiToken}}
|
||||
small!=env.t("APITokenWarning")
|
||||
br
|
||||
h3=env.t('thirdPartyApps')
|
||||
ul
|
||||
li
|
||||
a(target='_blank' href='https://www.beeminder.com/habitica')=env.t('beeminder')
|
||||
br
|
||||
=env.t('beeminderDesc')
|
||||
li
|
||||
a(target='_blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb')=env.t('chromeChatExtension')
|
||||
br
|
||||
=env.t('chromeChatExtensionDesc')
|
||||
li
|
||||
a(target='_blank' ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
|
||||
br
|
||||
=env.t('dataToolDesc')
|
||||
li
|
||||
!=env.t('otherExtensions')
|
||||
br
|
||||
=env.t('otherDesc')
|
||||
|
||||
hr
|
||||
|
||||
h2=env.t('webhooks')
|
||||
table.table.table-striped
|
||||
thead(ng-if='user.webhooks.length')
|
||||
tr
|
||||
th=env.t('enabled')
|
||||
th=env.t('webhookURL')
|
||||
th
|
||||
tbody
|
||||
tr(ng-repeat="webhook in user.webhooks track by $index")
|
||||
td
|
||||
input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook, $index)')
|
||||
td
|
||||
input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook, $index)'}")
|
||||
td
|
||||
span.pull-left(ng-show='webhook._editing') *
|
||||
a.checklist-icons(ng-click='deleteWebhook(webhook, $index)')
|
||||
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
|
||||
tr
|
||||
td(colspan=2)
|
||||
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
|
||||
.form-group.col-sm-10
|
||||
input.form-control(type='url', ng-model='_newWebhook.url', placeholder=env.t('webhookURL'))
|
||||
.col-sm-2
|
||||
button.btn.btn-sm.btn-primary(type='submit')=env.t('add')
|
||||
script(type='text/ng-template', id='partials/options.settings.api.html')
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6
|
||||
h2=env.t('API')
|
||||
small=env.t('APIText')
|
||||
h6=env.t('userId')
|
||||
pre.prettyprint {{user.id}}
|
||||
h6=env.t('APIToken')
|
||||
pre.prettyprint {{User.settings.auth.apiToken}}
|
||||
small!=env.t("APITokenWarning", { hrefTechAssistanceEmail : '<a href="mailto:' + env.EMAILS.TECH_ASSISTANCE_EMAIL + '">' + env.EMAILS.TECH_ASSISTANCE_EMAIL + '</a>' })
|
||||
br
|
||||
h3=env.t('thirdPartyApps')
|
||||
ul
|
||||
li
|
||||
a(target='_blank' href='https://www.beeminder.com/habitica')=env.t('beeminder')
|
||||
br
|
||||
=env.t('beeminderDesc')
|
||||
li
|
||||
a(target='_blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb')=env.t('chromeChatExtension')
|
||||
br
|
||||
=env.t('chromeChatExtensionDesc')
|
||||
li
|
||||
a(target='_blank' ng-href='http://data.habitrpg.com?uuid={{user._id}}')=env.t('dataTool')
|
||||
br
|
||||
=env.t('dataToolDesc')
|
||||
li
|
||||
!=env.t('otherExtensions')
|
||||
br
|
||||
=env.t('otherDesc')
|
||||
|
||||
hr
|
||||
|
||||
h2=env.t('webhooks')
|
||||
table.table.table-striped
|
||||
thead(ng-if='user.webhooks.length')
|
||||
tr
|
||||
th=env.t('enabled')
|
||||
th=env.t('webhookURL')
|
||||
th
|
||||
tbody
|
||||
tr(ng-repeat="webhook in user.webhooks track by $index")
|
||||
td
|
||||
input(type='checkbox', ng-model='webhook.enabled', ng-change='saveWebhook(webhook, $index)')
|
||||
td
|
||||
input.form-control(type='url', ng-model='webhook.url', ng-change='webhook._editing=true', ui-keyup="{13:'saveWebhook(webhook, $index)'}")
|
||||
td
|
||||
span.pull-left(ng-show='webhook._editing') *
|
||||
a.checklist-icons(ng-click='deleteWebhook(webhook, $index)')
|
||||
span.glyphicon.glyphicon-trash(tooltip=env.t('delete'))
|
||||
tr
|
||||
td(colspan=2)
|
||||
form.form-horizontal(ng-submit='addWebhook(_newWebhook.url)')
|
||||
.form-group.col-sm-10
|
||||
input.form-control(type='url', ng-model='_newWebhook.url', placeholder=env.t('webhookURL'))
|
||||
.col-sm-2
|
||||
button.btn.btn-sm.btn-primary(type='submit')=env.t('add')
|
||||
|
||||
@@ -87,7 +87,9 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
h3.panel-title
|
||||
=env.t('members')
|
||||
span(ng-if='group.type=="party" && (group.onlineUsers || group.onlineUsers == 0)')= ' (' + env.t('onlineCount', {count: "{{group.onlineUsers}}"}) + ')'
|
||||
button.pull-right.btn.btn-primary(ng-click="inviteOrStartParty(group)")=env.t("inviteFriends")
|
||||
button.pull-right.btn.btn-primary(ng-if='group.type=="party" ? (group.memberCount + group.invites.length < PARTY_LIMIT_MEMBERS) : true' ng-click="inviteOrStartParty(group)")=env.t("inviteFriends")
|
||||
.panel-heading(ng-if='group.type=="party"')
|
||||
p=env.t('partyMembersInfo', {memberCount: "{{group.memberCount}}", invitationCount:"{{group.invites.length}}", limitMembers:"{{PARTY_LIMIT_MEMBERS}}"})
|
||||
|
||||
.panel-body.modal-fixed-height
|
||||
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
|
||||
|
||||
@@ -36,6 +36,8 @@ footer.footer(ng-controller='FooterCtrl')
|
||||
a(href='/static/privacy')=env.t('companyPrivacy')
|
||||
li
|
||||
a(href='/static/terms')=env.t('companyTerms')
|
||||
li
|
||||
a(href='/static/press-kit')=env.t('presskit')
|
||||
li
|
||||
a(href='/static/contact')=env.t('contactUs')
|
||||
li
|
||||
|
||||
@@ -26,6 +26,7 @@ include ./login-incentives-reward-unlocked.jade
|
||||
include ./generic.jade
|
||||
include ./tasks-edit.jade
|
||||
include ./task-notes.jade
|
||||
include ./task-extra-notes.jade
|
||||
|
||||
//- Settings
|
||||
script(type='text/ng-template', id='modals/change-day-start.html')
|
||||
|
||||
@@ -79,6 +79,9 @@ script(type='text/ng-template', id='modals/send-gift.html')
|
||||
.btn-group
|
||||
a.btn.btn-default(ng-class="{active:gift.gems.fromBalance}", ng-click="gift.gems.fromBalance=true")=env.t('sendGiftFromBalance')
|
||||
a.btn.btn-default(ng-class="{active:!gift.gems.fromBalance}", ng-click="gift.gems.fromBalance=false")=env.t('sendGiftPurchase')
|
||||
.row
|
||||
.col-md-12
|
||||
p.small.muted!=env.t('gemGiftsAreOptional', { hrefTechAssistanceEmail : '<a href="mailto:' + env.EMAILS.TECH_ASSISTANCE_EMAIL + '">' + env.EMAILS.TECH_ASSISTANCE_EMAIL + '</a>'})
|
||||
|
||||
.panel.panel-default(class="{{gift.type=='subscription' ? 'panel-primary' : 'transparent'}}", ng-click='gift.type="subscription"')
|
||||
.panel-heading=env.t('subscription')
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
script(id='modals/task-extra-notes.html', type='text/ng-template')
|
||||
.modal-header
|
||||
h4=env.t('taskNotes')
|
||||
|
||||
.modal-body
|
||||
div
|
||||
markdown(text='task.notes')
|
||||
|
||||
.modal-footer
|
||||
.btn.btn-primary(ng-click="$close()")=env.t('close')
|
||||
@@ -4,14 +4,14 @@
|
||||
span(ng-if='showDoubleTaskCounter(task, obj)')
|
||||
span(tooltip=env.t('habitCounterUp')) +{{task.counterUp}}|
|
||||
span(tooltip=env.t('habitCounterDown')) -{{task.counterDown}}
|
||||
|
||||
|
||||
span(ng-if='showSingleTaskCounter(task, obj)')
|
||||
span(tooltip=env.t('habitCounter')) {{task.up ? task.counterUp : task.counterDown}}
|
||||
|
||||
|
||||
// Due Date
|
||||
span(ng-if='task.type=="todo" && task.date')
|
||||
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}}
|
||||
|
||||
|
||||
// Approval requested
|
||||
|
|
||||
span(ng-show='task.group.approval.requested && !task.group.approval.approved')
|
||||
@@ -29,7 +29,7 @@
|
||||
// Icons only available if you own the tasks (aka, hidden from challenge stats)
|
||||
span(ng-if='!obj._locked')
|
||||
group-task-meta-actions(ng-if="!obj.auth && obj.purchased && obj.purchased.active", task='task', group='obj')
|
||||
|
||||
|
||||
a(ng-click='pushTask(task,$index,"top")', tooltip=env.t('pushTaskToTop'), ng-class="{'push-down': ctrlPressed}")
|
||||
span(ng-hide="ctrlPressed").glyphicon.glyphicon-open
|
||||
span(ng-show="ctrlPressed").glyphicon.glyphicon-save
|
||||
@@ -39,13 +39,13 @@
|
||||
a.badge(ng-if='task.checklist[0]', ng-class='{"badge-success":checklistCompletion(task.checklist) == task.checklist.length}', ng-click='collapseChecklist(task)', tooltip=env.t('expandCollapse'))
|
||||
|{{checklistCompletion(task.checklist)}}/{{task.checklist.length}}
|
||||
span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)')
|
||||
|
||||
|
||||
// edit
|
||||
a(ng-hide='checkGroupAccess && !checkGroupAccess(obj)', ng-click='editTask(task, user, Shared.taskClasses(task, user.filters, user.preferences.dayStart, user.lastCron, list.showCompleted, main))', tooltip=env.t('edit'))
|
||||
|
|
||||
span.glyphicon.glyphicon-pencil
|
||||
|
|
||||
|
||||
|
||||
//challenges
|
||||
span(ng-if='task.challenge.id')
|
||||
span(ng-if='task.challenge.broken')
|
||||
@@ -54,7 +54,7 @@
|
||||
span(ng-if='!task.challenge.broken')
|
||||
span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge'))
|
||||
|
|
||||
|
||||
|
||||
// delete
|
||||
a(ng-if='!task.challenge.id || (obj.leader && obj.leader.id === user._id)', ng-hide="(checkGroupAccess && !checkGroupAccess(obj))" ng-click='removeTask(task, obj)', tooltip=env.t('delete'))
|
||||
span.glyphicon.glyphicon-trash
|
||||
@@ -67,6 +67,6 @@
|
||||
// notes
|
||||
|
||||
// Make this icon available regardless of task ownership
|
||||
a.task-notes(ng-show='task.notes && !task._editing', ng-click='task.popoverOpen = !task.popoverOpen', popover-trigger='click', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}')
|
||||
a.task-notes(ng-show='task.notes && !task._editing', ng-click='showNoteDetails(task);', popover-trigger='hover', data-popover-html="{{task.notes | markdown}}", popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}')
|
||||
span.glyphicon.glyphicon-comment
|
||||
|
|
||||
|
||||
@@ -28,8 +28,8 @@ block content
|
||||
p.pagemeta
|
||||
=env.t('lastUpdated')
|
||||
|
|
||||
=env.t('February')
|
||||
| 28, 2016
|
||||
=env.t('March')
|
||||
| 30, 2017
|
||||
h2#welcome=env.t('commGuideHeadingWelcome')
|
||||
.clearfix
|
||||
img.pull-left(src='/community-guidelines-images/intro.png', alt='')
|
||||
|
||||
Reference in New Issue
Block a user