mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Thehollidayinn/group plans part 2 (#8262)
* Added all ui components back * Added group ui items back and initial group approval directive * Added approval list view with approving functionality * Added notification display for group approvals * Fixed linting issues * Removed expectation from beforeEach * Moved string to locale * Added per use group plan for stripe * Added tests for stripe group plan upgrade * Removed paypal option * Abstract sub blocks. Hit group sub block from user settings page. Added group subscriptin beneifts display * Fixed lint issue * Added pricing and adjusted styles * Moved text to translations * Added group email types * Fixed typo * Fixed group plan abstraction and other style issues * Fixed email unit test * Added type to group plan to filter our group plans * Removed dev protection from routes * Removed hard coding and fixed upgrade plan * Added error when group has subscription and tries to remove * Fixed payment unit tests * Added custom string and moved subscription check up in the logic * Added ability for old leader to delete subscription the created * Allowed old guild leader to edit their group subscription * Fixed linting and tests * Added group sub page to user sub settings * Added approval and group tasks requests back. Hid user group sub on profile * Added group tasks sync after adding to allow for editing * Fixed promise chain when resolving group * Added approvals to group promise chain * Ensured compelted group todos are not delted at cron * Updated copy and other minor styles * Added group field to tags and recolored group tag. * Added chat message when task is claimed * Preventing task scoring when approval is needed * Added approval requested indicator * Updated column with for tasks on group page * Added checklist sync on assign * Added sync for checklist items * Added checkilist sync when task is updated * Added checklist sync remove * Sanatized group tasks when updated * Fixed lint issues * Added instant scoring of approved task * Added task modal * Fixed editing of challenge and group tasks * Added cancel button * Added add new checklist option to update sync * Added remove for checklist * Added checklist update * Added difference check and sync for checklist if there is a diff * Fixed task syncing * Fixed linting issues * Fixed styles and karma tests * Fixed minor style issues * Fixed obj transfer on scope * Fixed broken tests * Added new benefits page * Updated group page styles * Updated benefits page style * Added translations * Prevented sync with empty trask list * Added task title to edit modal * Added new group plans page and upgrade redirect * Added group plans redirect to upgrade * Fixed party home page being hidden and home button click * Fixed dynamic changing of task status and grey popup * Fixed tag editing * Hid benifites information if group has subscription * Added quotes to task name * Fixed issue with assigning multiple users * Added new group plans ctrl * Hid menu from public guilds * Fixed task sync issue * Updated placeholder for assign field * Added correct cost to subscribe details * Hid create, edit, delete task options from non group leaders * Prevented some front end modifications to group tasks * Hid tags option from group original task * Added refresh for approvals and group tasks * Prepend new group tasks * Fix last checklist item sync * Fixed casing issue with tags * Added claimed by message on hover * Prevent user from deleting assigned task * Added single route for group plan sign up and payments * Abstracted stripe payments and added initial tests * Abstracted amazon and added initial tests * Fixed create group message * Update group id check and return group * Updated to use the new returned group * Fixed linting and promise issues * Fixed broken leave test after merge issue * Fixed undefined approval error and editing/deleting challenge tasks * Add pricing to group plans, removed confirmation, and fixed redirect after payment * Updated group plan cost text
This commit is contained in:
@@ -207,6 +207,7 @@
|
|||||||
"selenium-server": "2.53.0",
|
"selenium-server": "2.53.0",
|
||||||
"sinon": "^1.17.2",
|
"sinon": "^1.17.2",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
|
"sinon-stub-promise": "^4.0.0",
|
||||||
"superagent-defaults": "^0.1.13",
|
"superagent-defaults": "^0.1.13",
|
||||||
"vinyl-transform": "^1.0.0",
|
"vinyl-transform": "^1.0.0",
|
||||||
"webpack-dev-middleware": "^1.4.0",
|
"webpack-dev-middleware": "^1.4.0",
|
||||||
|
|||||||
@@ -69,4 +69,16 @@ describe('DELETE /tasks/:id', () => {
|
|||||||
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
expect(syncedTask.group.broken).to.equal('TASK_DELETED');
|
||||||
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
expect(member2SyncedTask.group.broken).to.equal('TASK_DELETED');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prevents a user from deleting a task they are assigned to', async () => {
|
||||||
|
let memberTasks = await member.get('/tasks/user');
|
||||||
|
let syncedTask = find(memberTasks, findAssignedTask);
|
||||||
|
|
||||||
|
await expect(member.del(`/tasks/${syncedTask._id}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('cantDeleteAssignedGroupTasks'),
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,9 +59,11 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
|||||||
|
|
||||||
await member.sync();
|
await member.sync();
|
||||||
|
|
||||||
expect(member.notifications.length).to.equal(1);
|
expect(member.notifications.length).to.equal(2);
|
||||||
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
|
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
|
||||||
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved'));
|
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||||
|
expect(member.notifications[1].type).to.equal('SCORED_TASK');
|
||||||
|
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
|
||||||
|
|
||||||
expect(syncedTask.group.approval.approved).to.be.true;
|
expect(syncedTask.group.approval.approved).to.be.true;
|
||||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe('POST /tasks/:taskId', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows user to assign themselves', async () => {
|
it('allows user to assign themselves (claim)', async () => {
|
||||||
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
|
||||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||||
@@ -93,6 +93,14 @@ describe('POST /tasks/:taskId', () => {
|
|||||||
expect(syncedTask).to.exist;
|
expect(syncedTask).to.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends a message to the group when a user claims a task', async () => {
|
||||||
|
await member.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
|
||||||
|
let updateGroup = await user.get(`/groups/${guild._id}`);
|
||||||
|
|
||||||
|
expect(updateGroup.chat[0].text).to.equal(t('userIsClamingTask', {username: member.profile.name, task: task.text}));
|
||||||
|
});
|
||||||
|
|
||||||
it('assigns a task to a user', async () => {
|
it('assigns a task to a user', async () => {
|
||||||
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
await user.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,20 @@ import analytics from '../../../../../website/server/libs/analyticsService';
|
|||||||
import notifications from '../../../../../website/server/libs/pushNotifications';
|
import notifications from '../../../../../website/server/libs/pushNotifications';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import { model as Group } from '../../../../../website/server/models/group';
|
import { model as Group } from '../../../../../website/server/models/group';
|
||||||
|
import stripeModule from 'stripe';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
|
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
|
||||||
import {
|
import {
|
||||||
generateGroup,
|
generateGroup,
|
||||||
} from '../../../../helpers/api-unit.helper.js';
|
} from '../../../../helpers/api-unit.helper.js';
|
||||||
|
import i18n from '../../../../../website/common/script/i18n';
|
||||||
|
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||||
|
|
||||||
describe('payments/index', () => {
|
describe('payments/index', () => {
|
||||||
let user, group, data, plan;
|
let user, group, data, plan;
|
||||||
|
|
||||||
|
let stripe = stripeModule('test');
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
user = new User();
|
user = new User();
|
||||||
user.profile.name = 'sender';
|
user.profile.name = 'sender';
|
||||||
@@ -625,7 +630,40 @@ describe('payments/index', () => {
|
|||||||
await api.cancelSubscription(data);
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
expect(sender.sendTxn).to.be.calledOnce;
|
expect(sender.sendTxn).to.be.calledOnce;
|
||||||
expect(sender.sendTxn).to.be.calledWith(user, 'cancel-subscription');
|
expect(sender.sendTxn).to.be.calledWith(user, 'group-cancel-subscription');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prevents non group leader from manging subscription', async () => {
|
||||||
|
let groupMember = new User();
|
||||||
|
data.user = groupMember;
|
||||||
|
data.groupId = group._id;
|
||||||
|
|
||||||
|
await expect(api.cancelSubscription(data))
|
||||||
|
.eventually.be.rejected.and.to.eql({
|
||||||
|
httpCode: 401,
|
||||||
|
message: i18n.t('onlyGroupLeaderCanManageSubscription'),
|
||||||
|
name: 'NotAuthorized',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows old group leader to cancel if they created the subscription', async () => {
|
||||||
|
data.groupId = group._id;
|
||||||
|
data.sub = {
|
||||||
|
key: 'group_monthly',
|
||||||
|
};
|
||||||
|
data.paymentMethod = 'Payment Method';
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
let newLeader = new User();
|
||||||
|
updatedGroup.leader = newLeader._id;
|
||||||
|
await updatedGroup.save();
|
||||||
|
|
||||||
|
await api.cancelSubscription(data);
|
||||||
|
|
||||||
|
updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
|
||||||
|
expect(updatedGroup.purchased.plan.dateTerminated).to.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -732,4 +770,156 @@ describe('payments/index', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#upgradeGroupPlan', () => {
|
||||||
|
let spy;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||||
|
spy.returnsPromise().resolves([]);
|
||||||
|
data.groupId = group._id;
|
||||||
|
data.sub.quantity = 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sinon.restore(stripe.subscriptions.update);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates a group plan quantity', async () => {
|
||||||
|
data.paymentMethod = 'Stripe';
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||||
|
|
||||||
|
updatedGroup.memberCount += 1;
|
||||||
|
await updatedGroup.save();
|
||||||
|
|
||||||
|
await api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||||
|
|
||||||
|
expect(spy.calledOnce).to.be.true;
|
||||||
|
expect(updatedGroup.purchased.plan.quantity).to.eql(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not update a group plan quantity that has a payment method other than stripe', async () => {
|
||||||
|
await api.createSubscription(data);
|
||||||
|
|
||||||
|
let updatedGroup = await Group.findById(group._id).exec();
|
||||||
|
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||||
|
|
||||||
|
updatedGroup.memberCount += 1;
|
||||||
|
await updatedGroup.save();
|
||||||
|
|
||||||
|
await api.updateStripeGroupPlan(updatedGroup, stripe);
|
||||||
|
|
||||||
|
expect(spy.calledOnce).to.be.false;
|
||||||
|
expect(updatedGroup.purchased.plan.quantity).to.eql(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('payWithStripe', () => {
|
||||||
|
let spy;
|
||||||
|
let stripeCreateCustomerSpy;
|
||||||
|
let createSubSpy;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
spy = sinon.stub(stripe.subscriptions, 'update');
|
||||||
|
spy.returnsPromise().resolves;
|
||||||
|
|
||||||
|
stripeCreateCustomerSpy = sinon.stub(stripe.customers, 'create');
|
||||||
|
let stripCustomerResponse = {
|
||||||
|
subscriptions: {
|
||||||
|
data: [{id: 'test-id'}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
stripeCreateCustomerSpy.returnsPromise().resolves(stripCustomerResponse);
|
||||||
|
|
||||||
|
createSubSpy = sinon.stub(api, 'createSubscription');
|
||||||
|
createSubSpy.returnsPromise().resolves({});
|
||||||
|
|
||||||
|
data.groupId = group._id;
|
||||||
|
data.sub.quantity = 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
sinon.restore(stripe.subscriptions.update);
|
||||||
|
stripe.customers.create.restore();
|
||||||
|
api.createSubscription.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes with stripe', async () => {
|
||||||
|
let token = 'test-token';
|
||||||
|
let gift;
|
||||||
|
let sub = data.sub;
|
||||||
|
let groupId = group._id;
|
||||||
|
let email = 'test@test.com';
|
||||||
|
let headers = {};
|
||||||
|
let coupon;
|
||||||
|
|
||||||
|
await api.payWithStripe([
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
gift,
|
||||||
|
sub,
|
||||||
|
groupId,
|
||||||
|
email,
|
||||||
|
headers,
|
||||||
|
coupon,
|
||||||
|
], stripe);
|
||||||
|
|
||||||
|
expect(stripeCreateCustomerSpy.calledOnce).to.be.true;
|
||||||
|
expect(createSubSpy.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribeWithAmazon', () => {
|
||||||
|
let amazonSetBillingAgreementDetailsSpy;
|
||||||
|
let amazonConfirmBillingAgreementSpy;
|
||||||
|
let amazongAuthorizeOnBillingAgreementSpy;
|
||||||
|
let createSubSpy;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
amazonSetBillingAgreementDetailsSpy = sinon.stub(amzLib, 'setBillingAgreementDetails');
|
||||||
|
amazonSetBillingAgreementDetailsSpy.returnsPromise().resolves({});
|
||||||
|
|
||||||
|
amazonConfirmBillingAgreementSpy = sinon.stub(amzLib, 'confirmBillingAgreement');
|
||||||
|
amazonConfirmBillingAgreementSpy.returnsPromise().resolves({});
|
||||||
|
|
||||||
|
amazongAuthorizeOnBillingAgreementSpy = sinon.stub(amzLib, 'authorizeOnBillingAgreement');
|
||||||
|
amazongAuthorizeOnBillingAgreementSpy.returnsPromise().resolves({});
|
||||||
|
|
||||||
|
createSubSpy = sinon.stub(api, 'createSubscription');
|
||||||
|
createSubSpy.returnsPromise().resolves({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
amzLib.setBillingAgreementDetails.restore();
|
||||||
|
amzLib.confirmBillingAgreement.restore();
|
||||||
|
amzLib.authorizeOnBillingAgreement.restore();
|
||||||
|
api.createSubscription.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes with stripe', async () => {
|
||||||
|
let billingAgreementId = 'billingAgreementId';
|
||||||
|
let sub = data.sub;
|
||||||
|
let coupon;
|
||||||
|
let groupId = group._id;
|
||||||
|
let headers = {};
|
||||||
|
|
||||||
|
await api.subscribeWithAmazon([
|
||||||
|
billingAgreementId,
|
||||||
|
sub,
|
||||||
|
coupon,
|
||||||
|
sub,
|
||||||
|
user,
|
||||||
|
groupId,
|
||||||
|
headers,
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(amazonSetBillingAgreementDetailsSpy.calledOnce).to.be.true;
|
||||||
|
expect(amazonConfirmBillingAgreementSpy.calledOnce).to.be.true;
|
||||||
|
expect(amazongAuthorizeOnBillingAgreementSpy.calledOnce).to.be.true;
|
||||||
|
expect(createSubSpy.calledOnce).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { sleep } from '../../../../helpers/api-unit.helper';
|
import { sleep } from '../../../../helpers/api-unit.helper';
|
||||||
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import { BadRequest } from '../../../../../website/server/libs/errors';
|
import {
|
||||||
|
BadRequest,
|
||||||
|
} from '../../../../../website/server/libs/errors';
|
||||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||||
import * as email from '../../../../../website/server/libs/email';
|
import * as email from '../../../../../website/server/libs/email';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
import shared from '../../../../../website/common';
|
||||||
|
|
||||||
describe('Group Model', () => {
|
describe('Group Model', () => {
|
||||||
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
||||||
@@ -638,6 +641,22 @@ describe('Group Model', () => {
|
|||||||
expect(party).to.not.exist;
|
expect(party).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not delete a private group when the last member leaves and a subscription is active', async () => {
|
||||||
|
party.memberCount = 1;
|
||||||
|
party.purchased.plan.customerId = '110002222333';
|
||||||
|
|
||||||
|
await expect(party.leave(participatingMember))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
name: 'NotAuthorized',
|
||||||
|
httpCode: 401,
|
||||||
|
message: shared.i18n.t('cannotDeleteActiveGroup'),
|
||||||
|
});
|
||||||
|
|
||||||
|
party = await Group.findOne({_id: party._id});
|
||||||
|
expect(party).to.exist;
|
||||||
|
expect(party.memberCount).to.eql(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not delete a public group when the last member leaves', async () => {
|
it('does not delete a public group when the last member leaves', async () => {
|
||||||
party.privacy = 'public';
|
party.privacy = 'public';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { model as Challenge } from '../../../../../website/server/models/challen
|
|||||||
import { model as Group } from '../../../../../website/server/models/group';
|
import { model as Group } from '../../../../../website/server/models/group';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import * as Tasks from '../../../../../website/server/models/task';
|
import * as Tasks from '../../../../../website/server/models/task';
|
||||||
import { each, find } from 'lodash';
|
import { each, find, findIndex } from 'lodash';
|
||||||
|
|
||||||
describe('Group Task Methods', () => {
|
describe('Group Task Methods', () => {
|
||||||
let guild, leader, challenge, task;
|
let guild, leader, challenge, task;
|
||||||
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
|
|||||||
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
|
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
|
||||||
task.group.id = guild._id;
|
task.group.id = guild._id;
|
||||||
await task.save();
|
await task.save();
|
||||||
|
if (task.checklist) {
|
||||||
|
task.checklist.push({
|
||||||
|
text: 'Checklist Item 1',
|
||||||
|
completed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs an assigned task to a user', async () => {
|
it('syncs an assigned task to a user', async () => {
|
||||||
await guild.syncTask(task, leader);
|
await guild.syncTask(task, leader);
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let tagIndex = findIndex(updatedLeader.tags, {id: guild._id});
|
||||||
|
let newTag = updatedLeader.tags[tagIndex];
|
||||||
|
|
||||||
|
expect(newTag.id).to.equal(guild._id);
|
||||||
|
expect(newTag.name).to.equal(guild.name);
|
||||||
|
expect(newTag.group).to.equal(guild._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create tags for a user when task is synced', async () => {
|
||||||
|
await guild.syncTask(task, leader);
|
||||||
|
|
||||||
let updatedLeader = await User.findOne({_id: leader._id});
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
@@ -96,38 +114,124 @@ describe('Group Task Methods', () => {
|
|||||||
expect(syncedTask.text).to.equal(task.text);
|
expect(syncedTask.text).to.equal(task.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('syncs updated info for assigned task to all users', async () => {
|
it('syncs checklist items to an assigned user', async () => {
|
||||||
let newMember = new User({
|
|
||||||
guilds: [guild._id],
|
|
||||||
});
|
|
||||||
await newMember.save();
|
|
||||||
|
|
||||||
await guild.syncTask(task, leader);
|
await guild.syncTask(task, leader);
|
||||||
await guild.syncTask(task, newMember);
|
|
||||||
|
|
||||||
let updatedTaskName = 'Update Task name';
|
|
||||||
task.text = updatedTaskName;
|
|
||||||
task.group.approval.required = true;
|
|
||||||
|
|
||||||
await guild.updateTask(task);
|
|
||||||
|
|
||||||
let updatedLeader = await User.findOne({_id: leader._id});
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
|
|
||||||
let updatedMember = await User.findOne({_id: newMember._id});
|
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||||
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
|
||||||
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
|
||||||
|
|
||||||
expect(task.group.assignedUsers).to.contain(leader._id);
|
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||||
expect(syncedTask).to.exist;
|
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
|
||||||
expect(syncedTask.text).to.equal(task.text);
|
});
|
||||||
expect(syncedTask.group.approval.required).to.equal(true);
|
|
||||||
|
|
||||||
expect(task.group.assignedUsers).to.contain(newMember._id);
|
describe('syncs updated info', async() => {
|
||||||
expect(syncedMemberTask).to.exist;
|
let newMember;
|
||||||
expect(syncedMemberTask.text).to.equal(task.text);
|
|
||||||
expect(syncedMemberTask.group.approval.required).to.equal(true);
|
beforeEach(async () => {
|
||||||
|
newMember = new User({
|
||||||
|
guilds: [guild._id],
|
||||||
|
});
|
||||||
|
await newMember.save();
|
||||||
|
|
||||||
|
await guild.syncTask(task, leader);
|
||||||
|
await guild.syncTask(task, newMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs updated info for assigned task to all users', async () => {
|
||||||
|
let updatedTaskName = 'Update Task name';
|
||||||
|
task.text = updatedTaskName;
|
||||||
|
task.group.approval.required = true;
|
||||||
|
|
||||||
|
await guild.updateTask(task);
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
|
|
||||||
|
let updatedMember = await User.findOne({_id: newMember._id});
|
||||||
|
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||||
|
|
||||||
|
expect(task.group.assignedUsers).to.contain(leader._id);
|
||||||
|
expect(syncedTask).to.exist;
|
||||||
|
expect(syncedTask.text).to.equal(task.text);
|
||||||
|
expect(syncedTask.group.approval.required).to.equal(true);
|
||||||
|
|
||||||
|
expect(task.group.assignedUsers).to.contain(newMember._id);
|
||||||
|
expect(syncedMemberTask).to.exist;
|
||||||
|
expect(syncedMemberTask.text).to.equal(task.text);
|
||||||
|
expect(syncedMemberTask.group.approval.required).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs a new checklist item to all assigned users', async () => {
|
||||||
|
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||||
|
|
||||||
|
let newCheckListItem = {
|
||||||
|
text: 'Checklist Item 1',
|
||||||
|
completed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
task.checklist.push(newCheckListItem);
|
||||||
|
|
||||||
|
await guild.updateTask(task, {newCheckListItem});
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
|
|
||||||
|
let updatedMember = await User.findOne({_id: newMember._id});
|
||||||
|
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||||
|
|
||||||
|
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||||
|
expect(syncedTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||||
|
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||||
|
expect(syncedMemberTask.checklist[1].text).to.equal(task.checklist[1].text);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs updated info for checklist in assigned task to all users when flag is passed', async () => {
|
||||||
|
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||||
|
|
||||||
|
let updateCheckListText = 'Updated checklist item';
|
||||||
|
if (task.checklist) {
|
||||||
|
task.checklist[0].text = updateCheckListText;
|
||||||
|
}
|
||||||
|
|
||||||
|
await guild.updateTask(task, {updateCheckListItems: [task.checklist[0]]});
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
|
|
||||||
|
let updatedMember = await User.findOne({_id: newMember._id});
|
||||||
|
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||||
|
|
||||||
|
expect(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||||
|
expect(syncedTask.checklist[0].text).to.equal(updateCheckListText);
|
||||||
|
expect(syncedMemberTask.checklist.length).to.equal(task.checklist.length);
|
||||||
|
expect(syncedMemberTask.checklist[0].text).to.equal(updateCheckListText);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes a checklist item in assigned task to all users when flag is passed with checklist id', async () => {
|
||||||
|
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||||
|
|
||||||
|
await guild.updateTask(task, {removedCheckListItemId: task.checklist[0].id});
|
||||||
|
|
||||||
|
let updatedLeader = await User.findOne({_id: leader._id});
|
||||||
|
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||||
|
|
||||||
|
let updatedMember = await User.findOne({_id: newMember._id});
|
||||||
|
let updatedMemberTasks = await Tasks.Task.find({_id: { $in: updatedMember.tasksOrder[`${taskType}s`]}});
|
||||||
|
let syncedMemberTask = find(updatedMemberTasks, findLinkedTask);
|
||||||
|
|
||||||
|
expect(syncedTask.checklist.length).to.equal(0);
|
||||||
|
expect(syncedMemberTask.checklist.length).to.equal(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('removes an assigned task and unlinks assignees', async () => {
|
it('removes an assigned task and unlinks assignees', async () => {
|
||||||
|
|||||||
@@ -275,7 +275,8 @@ describe('Challenges Controller', function() {
|
|||||||
describe('editTask', function() {
|
describe('editTask', function() {
|
||||||
it('is Tasks.editTask', function() {
|
it('is Tasks.editTask', function() {
|
||||||
inject(function(Tasks) {
|
inject(function(Tasks) {
|
||||||
expect(scope.editTask).to.eql(Tasks.editTask);
|
// @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
|
||||||
|
// expect(scope.editTask).to.eql(Tasks.editTask);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ describe('Tasks Controller', function() {
|
|||||||
describe('editTask', function() {
|
describe('editTask', function() {
|
||||||
it('is Tasks.editTask', function() {
|
it('is Tasks.editTask', function() {
|
||||||
inject(function(Tasks) {
|
inject(function(Tasks) {
|
||||||
expect(scope.editTask).to.eql(Tasks.editTask);
|
// @TODO: Currently we override the task function in the challenge ctrl, but we should abstract it again
|
||||||
|
// expect(scope.editTask).to.eql(Tasks.editTask);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ describe('Tasks Service', function() {
|
|||||||
rootScope.charts = {};
|
rootScope.charts = {};
|
||||||
tasks = Tasks;
|
tasks = Tasks;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rootScope.openModal = function () {};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls get user tasks endpoint', function() {
|
it('calls get user tasks endpoint', function() {
|
||||||
@@ -151,7 +153,6 @@ describe('Tasks Service', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('editTask', function() {
|
describe('editTask', function() {
|
||||||
|
|
||||||
var task;
|
var task;
|
||||||
|
|
||||||
beforeEach(function(){
|
beforeEach(function(){
|
||||||
|
|||||||
@@ -157,6 +157,16 @@ describe('shared.ops.scoreTask', () => {
|
|||||||
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
|
expect(ref.afterUser._tmp.quest.progressDelta).to.eql(secondTaskDelta);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not modify stats when task need approval', () => {
|
||||||
|
todo.group.approval.required = true;
|
||||||
|
options = { user: ref.afterUser, task: todo, direction: 'up', times: 5, cron: false };
|
||||||
|
scoreTask(options);
|
||||||
|
|
||||||
|
expect(ref.afterUser.stats.hp).to.eql(50);
|
||||||
|
expect(ref.afterUser.stats.exp).to.equal(ref.beforeUser.stats.exp);
|
||||||
|
expect(ref.afterUser.stats.gp).to.equal(ref.beforeUser.stats.gp);
|
||||||
|
});
|
||||||
|
|
||||||
context('habits', () => {
|
context('habits', () => {
|
||||||
it('up', () => {
|
it('up', () => {
|
||||||
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };
|
options = { user: ref.afterUser, task: habit, direction: 'up', times: 5, cron: false };
|
||||||
|
|||||||
@@ -13,5 +13,7 @@ chai.use(require('sinon-chai'));
|
|||||||
chai.use(require('chai-as-promised'));
|
chai.use(require('chai-as-promised'));
|
||||||
global.expect = chai.expect;
|
global.expect = chai.expect;
|
||||||
global.sinon = require('sinon');
|
global.sinon = require('sinon');
|
||||||
|
let sinonStubPromise = require('sinon-stub-promise');
|
||||||
|
sinonStubPromise(global.sinon);
|
||||||
global.sandbox = sinon.sandbox.create();
|
global.sandbox = sinon.sandbox.create();
|
||||||
global.Promise = Bluebird;
|
global.Promise = Bluebird;
|
||||||
|
|||||||
@@ -8,59 +8,67 @@
|
|||||||
// array of keywords and their associated color vars
|
// array of keywords and their associated color vars
|
||||||
$stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $good) (better $better) (best $best)
|
$stages = (worst $worst) (worse $worse) (bad $bad) (neutral $neutral) (good $good) (better $better) (best $best)
|
||||||
|
|
||||||
|
taskContainerStyles($stage)
|
||||||
|
background-color: $stage[1]
|
||||||
|
border: 1px solid shade($stage[1],10%)
|
||||||
|
.priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency
|
||||||
|
li
|
||||||
|
hrpg-button-color-mixin($stage[1])
|
||||||
|
button
|
||||||
|
&.active
|
||||||
|
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
|
||||||
|
background-color: darken($stage[1],5%) !important
|
||||||
|
&:focus
|
||||||
|
border: 1px solid darken($stage[1],30%)
|
||||||
|
outline: 0
|
||||||
|
.plusminus
|
||||||
|
.task-checker
|
||||||
|
label:after
|
||||||
|
border: 1px solid darken($stage[1], 30%) !important
|
||||||
|
input[type=checkbox]:checked + label:after
|
||||||
|
//border: 1px solid darken($stage[1], 50%) !important
|
||||||
|
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
|
||||||
|
background-color: darken($stage[1],5%) !important
|
||||||
|
.save-close, .task-checklist-edit li
|
||||||
|
hrpg-button-color-mixin(darken($stage[1],5%))
|
||||||
|
button
|
||||||
|
&:focus
|
||||||
|
border: 1px solid darken($stage[1],30%)
|
||||||
|
outline: 0
|
||||||
|
.task-actions
|
||||||
|
background-color: darken($stage[1], 30%)
|
||||||
|
.action-yesno label,
|
||||||
|
.task-action-btn,
|
||||||
|
.task-actions a
|
||||||
|
background-color: darken($stage[1], 30%)
|
||||||
|
&:hover, &:focus
|
||||||
|
background-color: darken($stage[1], 40%)
|
||||||
|
input[type=checkbox].task-input:focus + label, input.habit:focus + a
|
||||||
|
background-color: darken($stage[1], 40%)
|
||||||
|
.task-actions a:nth-of-type(2)
|
||||||
|
border-top: 1px solid darken($stage[1],50%) // If there are two habit buttons (+ -), add a border to separate them
|
||||||
|
.task-options
|
||||||
|
background-color: $stage[1]
|
||||||
|
.option-group:not(.task-checklist)
|
||||||
|
border-bottom: 1px solid darken($stage[1], 15%)
|
||||||
|
.option-content
|
||||||
|
border-color: darken($stage[1], 16.18%) !important
|
||||||
|
&:hover
|
||||||
|
border-color: darken($stage[1], 32.8%) !important
|
||||||
|
&:focus
|
||||||
|
border-color: darken($stage[1], 61.8%) !important
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
|
||||||
// for each color stage, generate a named class w/ the appropriate color
|
// for each color stage, generate a named class w/ the appropriate color
|
||||||
for $stage in $stages
|
for $stage in $stages
|
||||||
.task-column:not(.rewards)
|
.task-column:not(.rewards)
|
||||||
.color-{$stage[0]}:not(.completed)
|
.color-{$stage[0]}:not(.completed)
|
||||||
background-color: $stage[1]
|
taskContainerStyles($stage)
|
||||||
border: 1px solid shade($stage[1],10%)
|
|
||||||
.priority-multiplier, .task-attributes, .repeat-days, .repeat-frequency
|
.task-modal
|
||||||
li
|
&.color-{$stage[0]}:not(.completed)
|
||||||
hrpg-button-color-mixin($stage[1])
|
taskContainerStyles($stage)
|
||||||
button
|
|
||||||
&.active
|
|
||||||
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
|
|
||||||
background-color: darken($stage[1],5%) !important
|
|
||||||
&:focus
|
|
||||||
border: 1px solid darken($stage[1],30%)
|
|
||||||
outline: 0
|
|
||||||
.plusminus
|
|
||||||
.task-checker
|
|
||||||
label:after
|
|
||||||
border: 1px solid darken($stage[1], 30%) !important
|
|
||||||
input[type=checkbox]:checked + label:after
|
|
||||||
//border: 1px solid darken($stage[1], 50%) !important
|
|
||||||
box-shadow: inset 0 0 0 1px darken($stage[1],40%) !important
|
|
||||||
background-color: darken($stage[1],5%) !important
|
|
||||||
.save-close, .task-checklist-edit li
|
|
||||||
hrpg-button-color-mixin(darken($stage[1],5%))
|
|
||||||
button
|
|
||||||
&:focus
|
|
||||||
border: 1px solid darken($stage[1],30%)
|
|
||||||
outline: 0
|
|
||||||
.task-actions
|
|
||||||
background-color: darken($stage[1], 30%)
|
|
||||||
.action-yesno label,
|
|
||||||
.task-action-btn,
|
|
||||||
.task-actions a
|
|
||||||
background-color: darken($stage[1], 30%)
|
|
||||||
&:hover, &:focus
|
|
||||||
background-color: darken($stage[1], 40%)
|
|
||||||
input[type=checkbox].task-input:focus + label, input.habit:focus + a
|
|
||||||
background-color: darken($stage[1], 40%)
|
|
||||||
.task-actions a:nth-of-type(2)
|
|
||||||
border-top: 1px solid darken($stage[1],50%) // If there are two habit buttons (+ -), add a border to separate them
|
|
||||||
.task-options
|
|
||||||
background-color: $stage[1]
|
|
||||||
.option-group:not(.task-checklist)
|
|
||||||
border-bottom: 1px solid darken($stage[1], 15%)
|
|
||||||
.option-content
|
|
||||||
border-color: darken($stage[1], 16.18%) !important
|
|
||||||
&:hover
|
|
||||||
border-color: darken($stage[1], 32.8%) !important
|
|
||||||
&:focus
|
|
||||||
border-color: darken($stage[1], 61.8%) !important
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
// completed has to be outside the loop to override the color class
|
// completed has to be outside the loop to override the color class
|
||||||
.completed
|
.completed
|
||||||
@@ -366,6 +374,12 @@ for $stage in $stages
|
|||||||
text-align: center
|
text-align: center
|
||||||
opacity: 0.75
|
opacity: 0.75
|
||||||
|
|
||||||
|
// Group yesno
|
||||||
|
.group-yesno
|
||||||
|
label:hover:after, input[type=checkbox]:checked + label:after
|
||||||
|
content: "" !important
|
||||||
|
opacity: 1 !important
|
||||||
|
|
||||||
|
|
||||||
// secondary task commands
|
// secondary task commands
|
||||||
// -----------------------
|
// -----------------------
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ window.habitrpg = angular.module('habitrpg',
|
|||||||
title: env.t('titlePatrons')
|
title: env.t('titlePatrons')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
.state('options.social.groupPlans', {
|
||||||
|
url: '/group-plans',
|
||||||
|
templateUrl: "partials/options.social.groupPlans.html",
|
||||||
|
controller: 'GroupPlansCtrl',
|
||||||
|
title: env.t('groupPlansTitle')
|
||||||
|
})
|
||||||
|
|
||||||
.state('options.social.guilds', {
|
.state('options.social.guilds', {
|
||||||
url: '/guilds',
|
url: '/guilds',
|
||||||
templateUrl: "partials/options.social.guilds.html",
|
templateUrl: "partials/options.social.guilds.html",
|
||||||
@@ -155,38 +162,55 @@ window.habitrpg = angular.module('habitrpg',
|
|||||||
templateUrl: "partials/options.social.guilds.create.html",
|
templateUrl: "partials/options.social.guilds.create.html",
|
||||||
title: env.t('titleGuilds')
|
title: env.t('titleGuilds')
|
||||||
})
|
})
|
||||||
|
|
||||||
.state('options.social.guilds.detail', {
|
.state('options.social.guilds.detail', {
|
||||||
url: '/:gid',
|
url: '/:gid',
|
||||||
templateUrl: 'partials/options.social.guilds.detail.html',
|
templateUrl: 'partials/options.social.guilds.detail.html',
|
||||||
title: env.t('titleGuilds'),
|
title: env.t('titleGuilds'),
|
||||||
controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks',
|
controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks', 'User', '$location',
|
||||||
function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks) {
|
function($scope, Groups, Chat, $stateParams, Members, Challenges, Tasks, User, $location) {
|
||||||
|
$scope.groupPanel = 'chat';
|
||||||
|
$scope.upgrade = false;
|
||||||
|
|
||||||
|
// @TODO: Move this to service or single http request
|
||||||
Groups.Group.get($stateParams.gid)
|
Groups.Group.get($stateParams.gid)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
$scope.obj = $scope.group = response.data.data;
|
$scope.obj = $scope.group = response.data.data;
|
||||||
Chat.markChatSeen($scope.group._id);
|
return Chat.markChatSeen($scope.group._id);
|
||||||
Members.getGroupMembers($scope.group._id)
|
|
||||||
.then(function (response) {
|
|
||||||
$scope.group.members = response.data.data;
|
|
||||||
});
|
|
||||||
Members.getGroupInvites($scope.group._id)
|
|
||||||
.then(function (response) {
|
|
||||||
$scope.group.invites = response.data.data;
|
|
||||||
});
|
|
||||||
Challenges.getGroupChallenges($scope.group._id)
|
|
||||||
.then(function (response) {
|
|
||||||
$scope.group.challenges = response.data.data;
|
|
||||||
});
|
|
||||||
//@TODO: Add this back when group tasks go live
|
|
||||||
// Tasks.getGroupTasks($scope.group._id);
|
|
||||||
// .then(function (response) {
|
|
||||||
// var tasks = response.data.data;
|
|
||||||
// tasks.forEach(function (element, index, array) {
|
|
||||||
// if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
|
|
||||||
// $scope.group[element.type + 's'].push(element);
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
})
|
})
|
||||||
|
.then (function () {
|
||||||
|
return Members.getGroupMembers($scope.group._id);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
$scope.group.members = response.data.data;
|
||||||
|
|
||||||
|
return Members.getGroupInvites($scope.group._id);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
$scope.group.invites = response.data.data;
|
||||||
|
|
||||||
|
return Challenges.getGroupChallenges($scope.group._id);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
$scope.group.challenges = response.data.data;
|
||||||
|
|
||||||
|
return Tasks.getGroupTasks($scope.group._id);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
var tasks = response.data.data;
|
||||||
|
tasks.forEach(function (element, index, array) {
|
||||||
|
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
|
||||||
|
$scope.group[element.type + 's'].unshift(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.group.approvals = [];
|
||||||
|
if (User.user._id === $scope.group.leader._id) {
|
||||||
|
return Tasks.getGroupApprovals($scope.group._id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
if (response) $scope.group.approvals = response.data.data;
|
||||||
|
});
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
habitrpg.controller('GroupApprovalsCtrl', ['$scope', 'Tasks',
|
habitrpg.controller('GroupApprovalsCtrl', ['$scope', 'Tasks',
|
||||||
function ($scope, Tasks) {
|
function ($scope, Tasks) {
|
||||||
$scope.approvals = [];
|
$scope.approve = function (taskId, userId, username, $index) {
|
||||||
|
if (!confirm(env.t('confirmTaskApproval', {username: username}))) return;
|
||||||
// Tasks.getGroupApprovals($scope.group._id)
|
|
||||||
// .then(function (response) {
|
|
||||||
// $scope.approvals = response.data.data;
|
|
||||||
// });
|
|
||||||
|
|
||||||
$scope.approve = function (taskId, userId, $index) {
|
|
||||||
if (!confirm(env.t('confirmTaskApproval'))) return;
|
|
||||||
Tasks.approve(taskId, userId)
|
Tasks.approve(taskId, userId)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
$scope.approvals.splice($index, 1);
|
$scope.group.approvals.splice($index, 1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.approvalTitle = function (approval) {
|
$scope.approvalTitle = function (approval) {
|
||||||
return env.t('approvalTitle', {text: approval.text, userName: approval.userId.profile.name});
|
return env.t('approvalTitle', {text: approval.text, userName: approval.userId.profile.name});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.refreshApprovals = function () {
|
||||||
|
$scope.loading = true;
|
||||||
|
Tasks.getGroupApprovals($scope.group._id)
|
||||||
|
.then(function (response) {
|
||||||
|
if (response) $scope.group.approvals = response.data.data;
|
||||||
|
$scope.loading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -27,12 +27,17 @@
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
var currentTags = [];
|
var currentTags = [];
|
||||||
_.each(scope.task.group.assignedUsers, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) })
|
_.each(scope.task.group.assignedUsers, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) });
|
||||||
|
|
||||||
|
var allowedTags = [];
|
||||||
|
_.each(scope.task.group.members, function(userId) { currentTags.push(memberIdToProfileNameMap[userId]) });
|
||||||
|
|
||||||
var taggle = new Taggle('taggle', {
|
var taggle = new Taggle('taggle', {
|
||||||
tags: currentTags,
|
tags: currentTags,
|
||||||
allowedTags: currentTags,
|
allowedTags: allowedTags,
|
||||||
allowDuplicates: false,
|
allowDuplicates: false,
|
||||||
|
preserveCase: true,
|
||||||
|
placeholder: window.env.t('assignFieldPlaceholder'),
|
||||||
onBeforeTagAdd: function(event, tag) {
|
onBeforeTagAdd: function(event, tag) {
|
||||||
return confirm(window.env.t('confirmAddTag', {tag: tag}));
|
return confirm(window.env.t('confirmAddTag', {tag: tag}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ habitrpg.controller('GroupTaskActionsCtrl', ['$scope', 'Shared', 'Tasks', 'User'
|
|||||||
function ($scope, Shared, Tasks, User) {
|
function ($scope, Shared, Tasks, User) {
|
||||||
$scope.assignedMembers = [];
|
$scope.assignedMembers = [];
|
||||||
$scope.user = User.user;
|
$scope.user = User.user;
|
||||||
|
|
||||||
$scope.task._edit.requiresApproval = false;
|
$scope.task._edit.requiresApproval = false;
|
||||||
if ($scope.task.group.approval.required) {
|
if ($scope.task.group.approval.required) {
|
||||||
$scope.task._edit.requiresApproval = $scope.task.group.approval.required;
|
$scope.task._edit.requiresApproval = $scope.task.group.approval.required;
|
||||||
|
|||||||
@@ -1,17 +1,65 @@
|
|||||||
habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) {
|
habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', function ($scope, Shared, Tasks, User) {
|
||||||
$scope.editTask = Tasks.editTask;
|
function handleGetGroupTasks (response) {
|
||||||
|
var group = $scope.obj;
|
||||||
|
|
||||||
|
var tasks = response.data.data;
|
||||||
|
|
||||||
|
if (tasks.length === 0) return;
|
||||||
|
|
||||||
|
// @TODO: We need to get the task information from createGroupTasks rather than resyncing
|
||||||
|
group['habits'] = [];
|
||||||
|
group['dailys'] = [];
|
||||||
|
group['todos'] = [];
|
||||||
|
group['rewards'] = [];
|
||||||
|
|
||||||
|
tasks.forEach(function (element, index, array) {
|
||||||
|
if (!$scope.group[element.type + 's']) $scope.group[element.type + 's'] = [];
|
||||||
|
$scope.group[element.type + 's'].unshift(element);
|
||||||
|
})
|
||||||
|
|
||||||
|
$scope.loading = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.refreshTasks = function () {
|
||||||
|
$scope.loading = true;
|
||||||
|
Tasks.getGroupTasks($scope.group._id)
|
||||||
|
.then(handleGetGroupTasks);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task Edit functions
|
||||||
|
*/
|
||||||
|
|
||||||
$scope.toggleBulk = Tasks.toggleBulk;
|
$scope.toggleBulk = Tasks.toggleBulk;
|
||||||
$scope.cancelTaskEdit = Tasks.cancelTaskEdit;
|
$scope.cancelTaskEdit = Tasks.cancelTaskEdit;
|
||||||
|
|
||||||
|
$scope.editTask = function (task, user, taskStatus) {
|
||||||
|
Tasks.editTask(task, user, taskStatus, $scope);
|
||||||
|
};
|
||||||
|
|
||||||
function addTask (listDef, taskTexts) {
|
function addTask (listDef, taskTexts) {
|
||||||
taskTexts.forEach(function (taskText) {
|
taskTexts.forEach(function (taskText) {
|
||||||
var task = Shared.taskDefaults({text: taskText, type: listDef.type});
|
var task = Shared.taskDefaults({text: taskText, type: listDef.type});
|
||||||
|
|
||||||
//If the group has not been created, we bulk add tasks on save
|
//If the group has not been created, we bulk add tasks on save
|
||||||
var group = $scope.obj;
|
var group = $scope.obj;
|
||||||
if (group._id) Tasks.createGroupTasks(group._id, task);
|
if (!group._id) return;
|
||||||
if (!group[task.type + 's']) group[task.type + 's'] = [];
|
|
||||||
group[task.type + 's'].unshift(task);
|
Tasks.createGroupTasks(group._id, task)
|
||||||
|
.then(function () {
|
||||||
|
// Set up default group info on task. @TODO: Move this to Tasks.createGroupTasks
|
||||||
|
task.group = {
|
||||||
|
id: group._id,
|
||||||
|
approval: {required: false, approved: false, requested: false},
|
||||||
|
assignedUsers: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!group[task.type + 's']) group[task.type + 's'] = [];
|
||||||
|
group[task.type + 's'].unshift(task);
|
||||||
|
|
||||||
|
return Tasks.getGroupTasks($scope.group._id);
|
||||||
|
})
|
||||||
|
.then(handleGetGroupTasks);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,8 +76,21 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.saveTask = function(task, stayOpen, isSaveAndClose) {
|
$scope.saveTask = function(task, stayOpen, isSaveAndClose) {
|
||||||
Tasks.saveTask (task, stayOpen, isSaveAndClose);
|
// Check if we have a lingering checklist that the enter button did not trigger on
|
||||||
Tasks.updateTask(task._id, task);
|
var lastIndex = task._edit.checklist.length - 1;
|
||||||
|
var lastCheckListItem = task._edit.checklist[lastIndex];
|
||||||
|
if (lastCheckListItem && !lastCheckListItem.id && lastCheckListItem.text) {
|
||||||
|
Tasks.addChecklistItem(task._id, lastCheckListItem)
|
||||||
|
.then(function (response) {
|
||||||
|
task._edit.checklist[lastIndex] = response.data.data.checklist[lastIndex];
|
||||||
|
task.checklist[lastIndex] = response.data.data.checklist[lastIndex];
|
||||||
|
Tasks.saveTask(task, stayOpen, isSaveAndClose);
|
||||||
|
Tasks.updateTask(task._id, task);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Tasks.saveTask (task, stayOpen, isSaveAndClose);
|
||||||
|
Tasks.updateTask(task._id, task);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.shouldShow = function(task, list, prefs){
|
$scope.shouldShow = function(task, list, prefs){
|
||||||
@@ -63,9 +124,23 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
|
|||||||
*/
|
*/
|
||||||
$scope.addChecklist = Tasks.addChecklist;
|
$scope.addChecklist = Tasks.addChecklist;
|
||||||
|
|
||||||
$scope.addChecklistItem = Tasks.addChecklistItemToUI;
|
$scope.addChecklistItem = function addChecklistItemToUI(task, $event, $index) {
|
||||||
|
if (task._edit.checklist[$index].justAdded) return;
|
||||||
|
task._edit.checklist[$index].justAdded = true;
|
||||||
|
if (!task._edit.checklist[$index].id) {
|
||||||
|
Tasks.addChecklistItem (task._id, task._edit.checklist[$index])
|
||||||
|
.then(function (response) {
|
||||||
|
task._edit.checklist[$index] = response.data.data.checklist[$index];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Tasks.addChecklistItemToUI(task, $event, $index);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.removeChecklistItem = Tasks.removeChecklistItemFromUI;
|
$scope.removeChecklistItem = function (task, $event, $index, force) {
|
||||||
|
if (!task._edit.checklist[$index].id) return;
|
||||||
|
Tasks.removeChecklistItem (task._id, task._edit.checklist[$index].id);
|
||||||
|
Tasks.removeChecklistItemFromUI(task, $event, $index, force);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.swapChecklistItems = Tasks.swapChecklistItems;
|
$scope.swapChecklistItems = Tasks.swapChecklistItems;
|
||||||
|
|
||||||
@@ -78,4 +153,34 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
|
|||||||
//@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api
|
//@TODO: Currently the api save of the task is separate, so whenever we need to save the task we need to call the respective api
|
||||||
Tasks.updateTask(task._id, task);
|
Tasks.updateTask(task._id, task);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.checkGroupAccess = function (group) {
|
||||||
|
if (!group || !group.leader) return true;
|
||||||
|
if (User.user._id !== group.leader._id) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task Details
|
||||||
|
*/
|
||||||
|
$scope.taskPopover = function (task) {
|
||||||
|
if (task.popoverOpen) return '';
|
||||||
|
|
||||||
|
var content = task.notes;
|
||||||
|
|
||||||
|
if ($scope.group) {
|
||||||
|
var memberIdToProfileNameMap = _.object(_.map($scope.group.members, function(item) {
|
||||||
|
return [item.id, item.profile.name]
|
||||||
|
}));
|
||||||
|
|
||||||
|
var claimingUsers = [];
|
||||||
|
task.group.assignedUsers.forEach(function (userId) {
|
||||||
|
claimingUsers.push(memberIdToProfileNameMap[userId]);
|
||||||
|
})
|
||||||
|
|
||||||
|
if (claimingUsers.length > 0) content += window.env.t('claimedBy', {claimingUsers: claimingUsers.join(', ')});
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
return User.user.challenges.indexOf(challenge._id) !== -1;
|
return User.user.challenges.indexOf(challenge._id) !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.editTask = Tasks.editTask;
|
$scope.editTask = function (task, user, taskStatus) {
|
||||||
|
Tasks.editTask(task, user, taskStatus, $scope);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.cancelTaskEdit = Tasks.cancelTaskEdit;
|
$scope.cancelTaskEdit = Tasks.cancelTaskEdit;
|
||||||
|
|
||||||
$scope.canEdit = function(task) {
|
$scope.canEdit = function(task) {
|
||||||
@@ -313,6 +316,15 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
|||||||
|
|
||||||
$scope.toggleBulk = Tasks.toggleBulk;
|
$scope.toggleBulk = Tasks.toggleBulk;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task Details
|
||||||
|
*/
|
||||||
|
$scope.taskPopover = function (task) {
|
||||||
|
if (task.popoverOpen) return '';
|
||||||
|
var content = task.notes;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
--------------------------
|
--------------------------
|
||||||
Subscription
|
Subscription
|
||||||
|
|||||||
@@ -45,4 +45,8 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'Shared',
|
|||||||
User.addTag({body:{name: $scope._newTag.name, id: Shared.uuid()}});
|
User.addTag({body:{name: $scope._newTag.name, id: Shared.uuid()}});
|
||||||
$scope._newTag.name = '';
|
$scope._newTag.name = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.showChallengeClass = function (tag) {
|
||||||
|
return tag.challenge || tag.group;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
43
website/client-old/js/controllers/groupPlansCtrl.js
Normal file
43
website/client-old/js/controllers/groupPlansCtrl.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
A controller to manage the Group Plans page
|
||||||
|
*/
|
||||||
|
|
||||||
|
angular.module('habitrpg')
|
||||||
|
.controller("GroupPlansCtrl", ['$scope', '$window', 'Groups', 'Payments',
|
||||||
|
function($scope, $window, Groups, Payments) {
|
||||||
|
$scope.PAGES = {
|
||||||
|
BENEFITS: 'benefits',
|
||||||
|
CREATE_GROUP: 'create-group',
|
||||||
|
UPGRADE_GROUP: 'upgrade-group',
|
||||||
|
};
|
||||||
|
$scope.activePage = $scope.PAGES.BENEFITS;
|
||||||
|
$scope.newGroup = {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
};
|
||||||
|
$scope.PAYMENTS = {
|
||||||
|
AMAZON: 'amazon',
|
||||||
|
STRIPE: 'stripe',
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changePage = function (page) {
|
||||||
|
$scope.activePage = page;
|
||||||
|
$window.scrollTo(0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.newGroupIsReady = function () {
|
||||||
|
return $scope.newGroup.name && $scope.newGroup.description;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.createGroup = function () {
|
||||||
|
$scope.changePage($scope.PAGES.UPGRADE_GROUP);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.upgradeGroup = function (paymentType) {
|
||||||
|
var subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
|
||||||
|
if (paymentType === $scope.PAYMENTS.STRIPE) Payments.showStripe({subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
|
||||||
|
if (paymentType === $scope.PAYMENTS.AMAZON) Payments.amazonPayments.init({type: 'subscription', subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
|
||||||
|
};
|
||||||
|
}]);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
|
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
|
||||||
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
|
function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
|
||||||
$scope.isMemberOfPendingQuest = function (userid, group) {
|
$scope.isMemberOfPendingQuest = function (userid, group) {
|
||||||
if (!group.quest || !group.quest.members) return false;
|
if (!group.quest || !group.quest.members) return false;
|
||||||
if (group.quest.active) return false; // quest is started, not pending
|
if (group.quest.active) return false; // quest is started, not pending
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
|
|||||||
Groups.Group.create(group)
|
Groups.Group.create(group)
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
var createdGroup = response.data.data;
|
var createdGroup = response.data.data;
|
||||||
$rootScope.hardRedirect('/#/options/groups/guilds/' + createdGroup._id);
|
$rootScope.hardRedirect('/#/options/groups/guilds/' + createdGroup._id + '?upgrade=true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ angular.module('habitrpg')
|
|||||||
|
|
||||||
function selectNotificationValue(mysteryValue, invitationValue, cardValue, unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) {
|
function selectNotificationValue(mysteryValue, invitationValue, cardValue, unallocatedValue, messageValue, noneValue, groupApprovalRequested, groupApproved) {
|
||||||
var user = $scope.user;
|
var user = $scope.user;
|
||||||
|
|
||||||
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
|
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
|
||||||
return mysteryValue;
|
return mysteryValue;
|
||||||
} else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {
|
} else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
habitrpg.controller('NotificationCtrl',
|
habitrpg.controller('NotificationCtrl',
|
||||||
['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social',
|
['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social', 'Tasks',
|
||||||
function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social) {
|
function ($scope, $rootScope, Shared, Content, User, Guide, Notification, Analytics, Achievement, Social, Tasks) {
|
||||||
|
|
||||||
$rootScope.$watch('user.stats.hp', function (after, before) {
|
$rootScope.$watch('user.stats.hp', function (after, before) {
|
||||||
if (after <= 0){
|
if (after <= 0){
|
||||||
@@ -87,6 +87,8 @@ habitrpg.controller('NotificationCtrl',
|
|||||||
if (!after || after.length === 0) return;
|
if (!after || after.length === 0) return;
|
||||||
|
|
||||||
var notificationsToRead = [];
|
var notificationsToRead = [];
|
||||||
|
var scoreTaskNotification;
|
||||||
|
|
||||||
after.forEach(function (notification) {
|
after.forEach(function (notification) {
|
||||||
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||||
return;
|
return;
|
||||||
@@ -141,6 +143,9 @@ habitrpg.controller('NotificationCtrl',
|
|||||||
trasnferGroupNotification(notification);
|
trasnferGroupNotification(notification);
|
||||||
markAsRead = false;
|
markAsRead = false;
|
||||||
break;
|
break;
|
||||||
|
case 'SCORED_TASK':
|
||||||
|
scoreTaskNotification = notification;
|
||||||
|
break;
|
||||||
case 'LOGIN_INCENTIVE':
|
case 'LOGIN_INCENTIVE':
|
||||||
Notification.showLoginIncentive(User.user, notification.data, Social.loadWidgets);
|
Notification.showLoginIncentive(User.user, notification.data, Social.loadWidgets);
|
||||||
break;
|
break;
|
||||||
@@ -159,7 +164,17 @@ habitrpg.controller('NotificationCtrl',
|
|||||||
if (markAsRead) notificationsToRead.push(notification.id);
|
if (markAsRead) notificationsToRead.push(notification.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
User.readNotifications(notificationsToRead);
|
var userReadNotifsPromise = User.readNotifications(notificationsToRead);
|
||||||
|
|
||||||
|
if (userReadNotifsPromise) {
|
||||||
|
userReadNotifsPromise.then(function () {
|
||||||
|
if (scoreTaskNotification) {
|
||||||
|
Notification.markdown(scoreTaskNotification.data.message);
|
||||||
|
User.score({params:{task: scoreTaskNotification.data.scoreTask, direction: "up"}});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
User.user.notifications = []; // reset the notifications
|
User.user.notifications = []; // reset the notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
|
|||||||
$scope.type = 'party';
|
$scope.type = 'party';
|
||||||
$scope.text = window.env.t('party');
|
$scope.text = window.env.t('party');
|
||||||
$scope.group = {loadingParty: true};
|
$scope.group = {loadingParty: true};
|
||||||
|
$scope.groupPanel = 'chat';
|
||||||
|
|
||||||
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
|
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
|
||||||
$scope.loadWidgets = Social.loadWidgets;
|
$scope.loadWidgets = Social.loadWidgets;
|
||||||
|
|||||||
@@ -51,11 +51,17 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
|||||||
|
|
||||||
$scope.toggleBulk = Tasks.toggleBulk;
|
$scope.toggleBulk = Tasks.toggleBulk;
|
||||||
|
|
||||||
$scope.editTask = Tasks.editTask;
|
$scope.editTask = function (task, user, taskStatus) {
|
||||||
|
Tasks.editTask(task, user, taskStatus, $scope);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.canEdit = function(task) {
|
$scope.canEdit = function(task) {
|
||||||
// can't edit challenge tasks
|
// can't edit challenge tasks
|
||||||
return !task.challenge.id;
|
return !task.challenge.id && (!task.group || !task.group.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.checkGroupAccess = function (group) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.doubleClickTask = function (obj, task) {
|
$scope.doubleClickTask = function (obj, task) {
|
||||||
@@ -225,9 +231,10 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
|||||||
------------------------
|
------------------------
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$scope.shouldShow = function(task, list, prefs){
|
$scope.shouldShow = function(task, list, prefs) {
|
||||||
if (task._editing) // never hide a task while being edited
|
if (task._editing) // never hide a task while being edited
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true;
|
var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true;
|
||||||
switch (list.view) {
|
switch (list.view) {
|
||||||
case "yellowred": // Habits
|
case "yellowred": // Habits
|
||||||
@@ -324,4 +331,13 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
|||||||
|
|
||||||
return notes;
|
return notes;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Task Details
|
||||||
|
*/
|
||||||
|
$scope.taskPopover = function (task) {
|
||||||
|
if (task.popoverOpen) return '';
|
||||||
|
var content = task.notes;
|
||||||
|
return content;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -37,12 +37,23 @@ function($rootScope, User, $http, Content) {
|
|||||||
panelLabel: sub ? window.env.t('subscribe') : window.env.t('checkout'),
|
panelLabel: sub ? window.env.t('subscribe') : window.env.t('checkout'),
|
||||||
token: function(res) {
|
token: function(res) {
|
||||||
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
|
var url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
|
||||||
|
|
||||||
|
if (data.groupToCreate) {
|
||||||
|
url = '/api/v3/groups/create-plan?a=a';
|
||||||
|
res.groupToCreate = data.groupToCreate;
|
||||||
|
res.paymentType = 'Stripe';
|
||||||
|
}
|
||||||
|
|
||||||
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
|
if (data.gift) url += '&gift=' + Payments.encodeGift(data.uuid, data.gift);
|
||||||
if (data.subscription) url += '&sub='+sub.key;
|
if (data.subscription) url += '&sub='+sub.key;
|
||||||
if (data.coupon) url += '&coupon='+data.coupon;
|
if (data.coupon) url += '&coupon='+data.coupon;
|
||||||
if (data.groupId) url += '&groupId=' + data.groupId;
|
if (data.groupId) url += '&groupId=' + data.groupId;
|
||||||
$http.post(url, res).success(function() {
|
$http.post(url, res).success(function(response) {
|
||||||
window.location.reload(true);
|
if (response && response.data && response.data._id) {
|
||||||
|
$rootScope.hardRedirect('/#/options/groups/guilds/' + response.data._id);
|
||||||
|
} else {
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
}).error(function(res) {
|
}).error(function(res) {
|
||||||
alert(res.message);
|
alert(res.message);
|
||||||
});
|
});
|
||||||
@@ -116,6 +127,10 @@ function($rootScope, User, $http, Content) {
|
|||||||
Payments.amazonPayments.groupId = data.groupId;
|
Payments.amazonPayments.groupId = data.groupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.groupToCreate) {
|
||||||
|
Payments.amazonPayments.groupToCreate = data.groupToCreate;
|
||||||
|
}
|
||||||
|
|
||||||
Payments.amazonPayments.gift = data.gift;
|
Payments.amazonPayments.gift = data.gift;
|
||||||
Payments.amazonPayments.type = data.type;
|
Payments.amazonPayments.type = data.type;
|
||||||
|
|
||||||
@@ -255,14 +270,24 @@ function($rootScope, User, $http, Content) {
|
|||||||
} else if(Payments.amazonPayments.type === 'subscription') {
|
} else if(Payments.amazonPayments.type === 'subscription') {
|
||||||
var url = '/amazon/subscribe';
|
var url = '/amazon/subscribe';
|
||||||
|
|
||||||
|
if (Payments.amazonPayments.groupToCreate) {
|
||||||
|
url = '/api/v3/groups/create-plan';
|
||||||
|
}
|
||||||
|
|
||||||
$http.post(url, {
|
$http.post(url, {
|
||||||
billingAgreementId: Payments.amazonPayments.billingAgreementId,
|
billingAgreementId: Payments.amazonPayments.billingAgreementId,
|
||||||
subscription: Payments.amazonPayments.subscription,
|
subscription: Payments.amazonPayments.subscription,
|
||||||
coupon: Payments.amazonPayments.coupon,
|
coupon: Payments.amazonPayments.coupon,
|
||||||
groupId: Payments.amazonPayments.groupId,
|
groupId: Payments.amazonPayments.groupId,
|
||||||
}).success(function(){
|
groupToCreate: Payments.amazonPayments.groupToCreate,
|
||||||
|
paymentType: 'Amazon',
|
||||||
|
}).success(function(response) {
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
window.location.reload(true);
|
if (response && response.data && response.data._id) {
|
||||||
|
$rootScope.hardRedirect('/#/options/groups/guilds/' + response.data._id);
|
||||||
|
} else {
|
||||||
|
window.location.reload(true);
|
||||||
|
}
|
||||||
}).error(function(res){
|
}).error(function(res){
|
||||||
alert(res.message);
|
alert(res.message);
|
||||||
Payments.amazonPayments.reset();
|
Payments.amazonPayments.reset();
|
||||||
|
|||||||
@@ -245,12 +245,31 @@ angular.module('habitrpg')
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function editTask(task, user) {
|
function editTask(task, user, taskStatus, scopeInc) {
|
||||||
task._editing = true;
|
var modalScope = $rootScope.$new();
|
||||||
task._tags = !user.preferences.tagsCollapsed;
|
modalScope.task = task;
|
||||||
task._advanced = !user.preferences.advancedCollapsed;
|
modalScope.task._editing = true;
|
||||||
task._edit = angular.copy(task);
|
modalScope.task._tags = !user.preferences.tagsCollapsed;
|
||||||
|
modalScope.task._advanced = !user.preferences.advancedCollapsed;
|
||||||
|
modalScope.task._edit = angular.copy(task);
|
||||||
if($rootScope.charts[task._id]) $rootScope.charts[task.id] = false;
|
if($rootScope.charts[task._id]) $rootScope.charts[task.id] = false;
|
||||||
|
|
||||||
|
modalScope.taskStatus = taskStatus;
|
||||||
|
if (scopeInc) {
|
||||||
|
modalScope.saveTask = scopeInc.saveTask;
|
||||||
|
modalScope.addChecklist = scopeInc.addChecklist;
|
||||||
|
modalScope.addChecklistItem = scopeInc.addChecklistItem;
|
||||||
|
modalScope.removeChecklistItem = scopeInc.removeChecklistItem;
|
||||||
|
modalScope.swapChecklistItems = scopeInc.swapChecklistItems;
|
||||||
|
modalScope.navigateChecklist = scopeInc.navigateChecklist;
|
||||||
|
modalScope.checklistCompletion = scopeInc.checklistCompletion;
|
||||||
|
modalScope.canEdit = scopeInc.canEdit;
|
||||||
|
modalScope.updateTaskTags = scopeInc.updateTaskTags;
|
||||||
|
modalScope.obj = scopeInc.obj;
|
||||||
|
}
|
||||||
|
modalScope.cancelTaskEdit = cancelTaskEdit;
|
||||||
|
|
||||||
|
$rootScope.openModal('task-edit', {scope: modalScope, backdrop: 'static'});
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelTaskEdit(task) {
|
function cancelTaskEdit(task) {
|
||||||
@@ -289,7 +308,7 @@ angular.module('habitrpg')
|
|||||||
|
|
||||||
function focusChecklist(task, index) {
|
function focusChecklist(task, index) {
|
||||||
window.setTimeout(function(){
|
window.setTimeout(function(){
|
||||||
$('#task-'+task._id+' .checklist-form input[type="text"]')[index].focus();
|
$('#task-' + task._id + ' .checklist-form input[type="text"]')[index].focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ angular.module('habitrpg')
|
|||||||
},
|
},
|
||||||
|
|
||||||
readNotifications: function (notificationIds) {
|
readNotifications: function (notificationIds) {
|
||||||
UserNotifications.readNotifications(notificationIds);
|
return UserNotifications.readNotifications(notificationIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
addTag: function(data) {
|
addTag: function(data) {
|
||||||
|
|||||||
@@ -109,6 +109,7 @@
|
|||||||
"js/controllers/tavernCtrl.js",
|
"js/controllers/tavernCtrl.js",
|
||||||
"js/controllers/tasksCtrl.js",
|
"js/controllers/tasksCtrl.js",
|
||||||
"js/controllers/userCtrl.js",
|
"js/controllers/userCtrl.js",
|
||||||
|
"js/controllers/groupPlansCtrl.js",
|
||||||
|
|
||||||
"js/components/groupTasks/groupTasksController.js",
|
"js/components/groupTasks/groupTasksController.js",
|
||||||
"js/components/groupTasks/groupTasksDirective.js",
|
"js/components/groupTasks/groupTasksDirective.js",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"titleIndex": "Habitica | Your Life The Role Playing Game",
|
"titleIndex": "Habitica | Your Life The Role Playing Game",
|
||||||
"habitica": "Habitica",
|
"habitica": "Habitica",
|
||||||
"habiticaLink": "<a href='http://habitica.wikia.com/wiki/Habitica' target='_blank'>Habitica</a>",
|
"habiticaLink": "<a href='http://habitica.wikia.com/wiki/Habitica' target='_blank'>Habitica</a>",
|
||||||
|
|
||||||
"titleTasks": "Tasks",
|
"titleTasks": "Tasks",
|
||||||
"titleAvatar": "Avatar",
|
"titleAvatar": "Avatar",
|
||||||
"titleBackgrounds": "Backgrounds",
|
"titleBackgrounds": "Backgrounds",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"help": "Help",
|
"help": "Help",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"market": "Market",
|
"market": "Market",
|
||||||
|
"groupPlansTitle": "Group Plans",
|
||||||
"subscriberItem": "Mystery Item",
|
"subscriberItem": "Mystery Item",
|
||||||
"newSubscriberItem": "New Mystery Item",
|
"newSubscriberItem": "New Mystery Item",
|
||||||
"subscriberItemText": "Each month, subscribers will receive a mystery item. This is usually released about one week before the end of the month. See the wiki's 'Mystery Item' page for more information.",
|
"subscriberItemText": "Each month, subscribers will receive a mystery item. This is usually released about one week before the end of the month. See the wiki's 'Mystery Item' page for more information.",
|
||||||
@@ -194,5 +195,7 @@
|
|||||||
"you": "(you)",
|
"you": "(you)",
|
||||||
"enableDesktopNotifications": "Enable Desktop Notifications",
|
"enableDesktopNotifications": "Enable Desktop Notifications",
|
||||||
"online": "online",
|
"online": "online",
|
||||||
"onlineCount": "<%= count %> online"
|
"onlineCount": "<%= count %> online",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"userIdRequired": "User ID is required"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,224 +1,257 @@
|
|||||||
{
|
{
|
||||||
"tavern": "Tavern Chat",
|
"tavern": "Tavern Chat",
|
||||||
"innCheckOut": "Check Out of Inn",
|
"innCheckOut": "Check Out of Inn",
|
||||||
"innCheckIn": "Rest in the Inn",
|
"innCheckIn": "Rest in the Inn",
|
||||||
"innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
|
"innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
|
||||||
"innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
|
"innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
|
||||||
"lfgPosts": "Looking for Group (Party Wanted) Posts",
|
"lfgPosts": "Looking for Group (Party Wanted) Posts",
|
||||||
"tutorial": "Tutorial",
|
"tutorial": "Tutorial",
|
||||||
"glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
|
"glossary": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
|
||||||
"wiki": "Wiki",
|
"wiki": "Wiki",
|
||||||
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
|
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
|
||||||
"reportAP": "Report a Problem",
|
"reportAP": "Report a Problem",
|
||||||
"requestAF": "Request a Feature",
|
"requestAF": "Request a Feature",
|
||||||
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
|
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
|
||||||
"dataTool": "Data Display Tool",
|
"dataTool": "Data Display Tool",
|
||||||
"resources": "Resources",
|
"resources": "Resources",
|
||||||
"askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)",
|
"askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)",
|
||||||
"tavernAlert1": "To report a bug, visit",
|
"tavernAlert1": "To report a bug, visit",
|
||||||
"tavernAlert2": "the Report a Bug Guild",
|
"tavernAlert2": "the Report a Bug Guild",
|
||||||
"moderatorIntro1": "Tavern and guild moderators are: ",
|
"moderatorIntro1": "Tavern and guild moderators are: ",
|
||||||
"communityGuidelines": "Community Guidelines",
|
"communityGuidelines": "Community Guidelines",
|
||||||
"communityGuidelinesRead1": "Please read our",
|
"communityGuidelinesRead1": "Please read our",
|
||||||
"communityGuidelinesRead2": "before chatting.",
|
"communityGuidelinesRead2": "before chatting.",
|
||||||
"party": "Party",
|
"party": "Party",
|
||||||
"createAParty": "Create A Party",
|
"createAParty": "Create A Party",
|
||||||
"updatedParty": "Party settings updated.",
|
"updatedParty": "Party settings updated.",
|
||||||
"noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
|
"noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
|
||||||
"LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
|
"LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
|
||||||
"wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
|
"wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
|
||||||
"joinExistingParty": "Join Someone Else's Party",
|
"joinExistingParty": "Join Someone Else's Party",
|
||||||
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> before you can start a quest!",
|
"needPartyToStartQuest": "Whoops! You need to <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> before you can start a quest!",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"userId": "User ID",
|
"userId": "User ID",
|
||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"leave": "Leave",
|
"leave": "Leave",
|
||||||
"invitedTo": "Invited to <%= name %>",
|
"invitedTo": "Invited to <%= name %>",
|
||||||
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
|
"invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
|
||||||
"invitationAcceptedHeader": "Your Invitation has been Accepted",
|
"invitationAcceptedHeader": "Your Invitation has been Accepted",
|
||||||
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
|
"invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
|
||||||
"joinNewParty": "Join New Party",
|
"joinNewParty": "Join New Party",
|
||||||
"declineInvitation": "Decline Invitation",
|
"declineInvitation": "Decline Invitation",
|
||||||
"partyLoading1": "Your party is being summoned. Please wait...",
|
"partyLoading1": "Your party is being summoned. Please wait...",
|
||||||
"partyLoading2": "Your party is coming in from battle. Please wait...",
|
"partyLoading2": "Your party is coming in from battle. Please wait...",
|
||||||
"partyLoading3": "Your party is gathering. Please wait...",
|
"partyLoading3": "Your party is gathering. Please wait...",
|
||||||
"partyLoading4": "Your party is materializing. Please wait...",
|
"partyLoading4": "Your party is materializing. Please wait...",
|
||||||
"systemMessage": "System Message",
|
"systemMessage": "System Message",
|
||||||
"newMsg": "New message in \"<%= name %>\"",
|
"newMsg": "New message in \"<%= name %>\"",
|
||||||
"chat": "Chat",
|
"chat": "Chat",
|
||||||
"sendChat": "Send Chat",
|
"sendChat": "Send Chat",
|
||||||
"toolTipMsg": "Fetch Recent Messages",
|
"toolTipMsg": "Fetch Recent Messages",
|
||||||
"sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
|
"sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
|
||||||
"syncPartyAndChat": "Sync Party and Chat",
|
"syncPartyAndChat": "Sync Party and Chat",
|
||||||
"guildBankPop1": "Guild Bank",
|
"guildBankPop1": "Guild Bank",
|
||||||
"guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
|
"guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
|
||||||
"guildGems": "Guild Gems",
|
"guildGems": "Guild Gems",
|
||||||
"editGroup": "Edit Group",
|
"editGroup": "Edit Group",
|
||||||
"newGroupName": "<%= groupType %> Name",
|
"newGroupName": "<%= groupType %> Name",
|
||||||
"groupName": "Group Name",
|
"groupName": "Group Name",
|
||||||
"groupLeader": "Group Leader",
|
"groupLeader": "Group Leader",
|
||||||
"groupID": "Group ID",
|
"groupID": "Group ID",
|
||||||
"groupDescr": "Description shown in public Guilds list (Markdown OK)",
|
"groupDescr": "Description shown in public Guilds list (Markdown OK)",
|
||||||
"logoUrl": "Logo URL",
|
"logoUrl": "Logo URL",
|
||||||
"assignLeader": "Assign Group Leader",
|
"assignLeader": "Assign Group Leader",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"partyList": "Order for party members in header",
|
"partyList": "Order for party members in header",
|
||||||
"banTip": "Boot Member",
|
"banTip": "Boot Member",
|
||||||
"moreMembers": "more members",
|
"moreMembers": "more members",
|
||||||
"invited": "Invited",
|
"invited": "Invited",
|
||||||
"leaderMsg": "Message from group leader (Markdown OK)",
|
"leaderMsg": "Message from group leader (Markdown OK)",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"public": "Public",
|
"public": "Public",
|
||||||
"inviteOnly": "Invite Only",
|
"inviteOnly": "Invite Only",
|
||||||
"gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
|
"gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"publicGuilds": "Public Guilds",
|
"publicGuilds": "Public Guilds",
|
||||||
"createGuild": "Create Guild",
|
"createGuild": "Create Guild",
|
||||||
"guild": "Guild",
|
"guild": "Guild",
|
||||||
"guilds": "Guilds",
|
"guilds": "Guilds",
|
||||||
"guildsLink": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
|
"guildsLink": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
|
||||||
"sureKick": "Do you really want to remove this member from the party/guild?",
|
"sureKick": "Do you really want to remove this member from the party/guild?",
|
||||||
"optionalMessage": "Optional message",
|
"optionalMessage": "Optional message",
|
||||||
"yesRemove": "Yes, remove them",
|
"yesRemove": "Yes, remove them",
|
||||||
"foreverAlone": "Can't like your own message. Don't be that person.",
|
"foreverAlone": "Can't like your own message. Don't be that person.",
|
||||||
"sortLevel": "Sort by level",
|
"sortLevel": "Sort by level",
|
||||||
"sortRandom": "Sort randomly",
|
"sortRandom": "Sort randomly",
|
||||||
"sortPets": "Sort by number of pets",
|
"sortPets": "Sort by number of pets",
|
||||||
"sortName": "Sort by avatar name",
|
"sortName": "Sort by avatar name",
|
||||||
"sortBackgrounds": "Sort by background",
|
"sortBackgrounds": "Sort by background",
|
||||||
"sortHabitrpgJoined": "Sort by Habitica date joined",
|
"sortHabitrpgJoined": "Sort by Habitica date joined",
|
||||||
"sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
|
"sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
|
||||||
"ascendingSort": "Sort Ascending",
|
"ascendingSort": "Sort Ascending",
|
||||||
"descendingSort": "Sort Descending",
|
"descendingSort": "Sort Descending",
|
||||||
"confirmGuild": "Create Guild for 4 Gems?",
|
"confirmGuild": "Create Guild for 4 Gems?",
|
||||||
"leaveGroupCha": "Leave Guild challenges and...",
|
"leaveGroupCha": "Leave Guild challenges and...",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"leaveGroup": "Leave Guild?",
|
"leaveGroup": "Leave Guild?",
|
||||||
"leavePartyCha": "Leave party challenges and...",
|
"leavePartyCha": "Leave party challenges and...",
|
||||||
"leaveParty": "Leave party?",
|
"leaveParty": "Leave party?",
|
||||||
"sendPM": "Send private message",
|
"sendPM": "Send private message",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"messageSentAlert": "Message sent",
|
"messageSentAlert": "Message sent",
|
||||||
"pmHeading": "Private message to <%= name %>",
|
"pmHeading": "Private message to <%= name %>",
|
||||||
"pmsMarkedRead": "Your private messages have been marked as read",
|
"pmsMarkedRead": "Your private messages have been marked as read",
|
||||||
"clearAll": "Delete All Messages",
|
"clearAll": "Delete All Messages",
|
||||||
"confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
|
"confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
|
||||||
"optOutPopover": "Don't like private messages? Click to completely opt out",
|
"optOutPopover": "Don't like private messages? Click to completely opt out",
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
"unblock": "Un-block",
|
"unblock": "Un-block",
|
||||||
"pm-reply": "Send a reply",
|
"pm-reply": "Send a reply",
|
||||||
"inbox": "Inbox",
|
"inbox": "Inbox",
|
||||||
"messageRequired": "A message is required.",
|
"messageRequired": "A message is required.",
|
||||||
"toUserIDRequired": "A User ID is required",
|
"toUserIDRequired": "A User ID is required",
|
||||||
"gemAmountRequired": "A number of gems is required",
|
"gemAmountRequired": "A number of gems is required",
|
||||||
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
|
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
|
||||||
"privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!",
|
"privateMessageGiftIntro": "Hello <%= receiverName %>, <%= senderName %> has sent you ",
|
||||||
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
|
"privateMessageGiftGemsMessage": "<%= gemAmount %> gems!",
|
||||||
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
|
"privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
|
||||||
"abuseFlag": "Report violation of Community Guidelines",
|
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
|
||||||
"abuseFlagModalHeading": "Report <%= name %> for violation?",
|
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
|
||||||
"abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
|
"abuseFlag": "Report violation of Community Guidelines",
|
||||||
"abuseFlagModalButton": "Report Violation",
|
"abuseFlagModalHeading": "Report <%= name %> for violation?",
|
||||||
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
|
"abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
|
||||||
"abuseAlreadyReported": "You have already reported this message.",
|
"abuseFlagModalButton": "Report Violation",
|
||||||
"needsText": "Please type a message.",
|
"abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
|
||||||
"needsTextPlaceholder": "Type your message here.",
|
"abuseAlreadyReported": "You have already reported this message.",
|
||||||
"copyMessageAsToDo": "Copy message as To-Do",
|
"needsText": "Please type a message.",
|
||||||
"messageAddedAsToDo": "Message copied as To-Do.",
|
"needsTextPlaceholder": "Type your message here.",
|
||||||
"messageWroteIn": "<%= user %> wrote in <%= group %>",
|
"copyMessageAsToDo": "Copy message as To-Do",
|
||||||
"taskFromInbox": "<%= from %> wrote '<%= message %>'",
|
"messageAddedAsToDo": "Message copied as To-Do.",
|
||||||
"taskTextFromInbox": "Message from <%= from %>",
|
"messageWroteIn": "<%= user %> wrote in <%= group %>",
|
||||||
"msgPreviewHeading": "Message Preview",
|
"taskFromInbox": "<%= from %> wrote '<%= message %>'",
|
||||||
"leaderOnlyChallenges": "Only group leader can create challenges",
|
"taskTextFromInbox": "Message from <%= from %>",
|
||||||
"sendGift": "Send Gift",
|
"msgPreviewHeading": "Message Preview",
|
||||||
"inviteFriends": "Invite Friends",
|
"leaderOnlyChallenges": "Only group leader can create challenges",
|
||||||
"inviteByEmail": "Invite by Email",
|
"sendGift": "Send Gift",
|
||||||
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
|
"inviteFriends": "Invite Friends",
|
||||||
"inviteFriendsNow": "Invite Friends Now",
|
"inviteByEmail": "Invite by Email",
|
||||||
"inviteFriendsLater": "Invite Friends Later",
|
"inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
|
||||||
"inviteAlertInfo": "If you have friends already using Habitica, invite them by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
|
"inviteFriendsNow": "Invite Friends Now",
|
||||||
"inviteExistUser": "Invite Existing Users",
|
"inviteFriendsLater": "Invite Friends Later",
|
||||||
"byColon": "By:",
|
"inviteAlertInfo": "If you have friends already using Habitica, invite them by <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> here.",
|
||||||
"inviteNewUsers": "Invite New Users",
|
"inviteExistUser": "Invite Existing Users",
|
||||||
"sendInvitations": "Send Invitations",
|
"byColon": "By:",
|
||||||
"invitationsSent": "Invitations sent!",
|
"inviteNewUsers": "Invite New Users",
|
||||||
"invitationSent": "Invitation sent!",
|
"sendInvitations": "Send Invitations",
|
||||||
"inviteAlertInfo2": "Or share this link (copy/paste):",
|
"invitationsSent": "Invitations sent!",
|
||||||
"sendGiftHeading": "Send Gift to <%= name %>",
|
"invitationSent": "Invitation sent!",
|
||||||
"sendGiftGemsBalance": "From <%= number %> Gems",
|
"inviteAlertInfo2": "Or share this link (copy/paste):",
|
||||||
"sendGiftCost": "Total: $<%= cost %> USD",
|
"sendGiftHeading": "Send Gift to <%= name %>",
|
||||||
"sendGiftFromBalance": "From Balance",
|
"sendGiftGemsBalance": "From <%= number %> Gems",
|
||||||
"sendGiftPurchase": "Purchase",
|
"sendGiftCost": "Total: $<%= cost %> USD",
|
||||||
"sendGiftMessagePlaceholder": "Personal message (optional)",
|
"sendGiftFromBalance": "From Balance",
|
||||||
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
|
"sendGiftPurchase": "Purchase",
|
||||||
"battleWithFriends": "Battle Monsters With Friends",
|
"sendGiftMessagePlaceholder": "Personal message (optional)",
|
||||||
"startPartyWithFriends": "Start a Party with your friends!",
|
"sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
|
||||||
"startAParty": "Start a Party",
|
"battleWithFriends": "Battle Monsters With Friends",
|
||||||
"addToParty": "Add someone to your party",
|
"startPartyWithFriends": "Start a Party with your friends!",
|
||||||
"likePost": "Click if you like this post!",
|
"startAParty": "Start a Party",
|
||||||
"partyExplanation1": "Play Habitica with friends to stay accountable!",
|
"addToParty": "Add someone to your party",
|
||||||
"partyExplanation2": "Battle monsters and create Challenges!",
|
"likePost": "Click if you like this post!",
|
||||||
"partyExplanation3": "Invite friends now to earn a Quest Scroll!",
|
"partyExplanation1": "Play Habitica with friends to stay accountable!",
|
||||||
"wantToStartParty": "Do you want to start a party?",
|
"partyExplanation2": "Battle monsters and create Challenges!",
|
||||||
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
|
"partyExplanation3": "Invite friends now to earn a Quest Scroll!",
|
||||||
"nameYourParty": "Name your new party!",
|
"wantToStartParty": "Do you want to start a party?",
|
||||||
"partyEmpty": "You're the only one in your party. Invite your friends!",
|
"exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
|
||||||
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
|
"nameYourParty": "Name your new party!",
|
||||||
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
|
"partyEmpty": "You're the only one in your party. Invite your friends!",
|
||||||
"possessiveParty": "<%= name %>'s Party",
|
"partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
|
||||||
"requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.",
|
"guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
|
||||||
"partyUpName": "Party Up",
|
"possessiveParty": "<%= name %>'s Party",
|
||||||
"partyOnName": "Party On",
|
"requestAcceptGuidelines": "If you would like to post messages in the Tavern or any party or guild chat, please first read our <%= linkStart %>Community Guidelines<%= linkEnd %> and then click the button below to indicate that you accept them.",
|
||||||
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
|
"partyUpName": "Party Up",
|
||||||
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
|
"partyOnName": "Party On",
|
||||||
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
|
"partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
|
||||||
"groupIdRequired": "\"groupId\" must be a valid UUID",
|
"partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
|
||||||
"groupNotFound": "Group not found or you don't have access.",
|
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
|
||||||
"groupTypesRequired": "You must supply a valid \"type\" query string.",
|
"groupIdRequired": "\"groupId\" must be a valid UUID",
|
||||||
"questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
|
"groupNotFound": "Group not found or you don't have access.",
|
||||||
"cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
|
"groupTypesRequired": "You must supply a valid \"type\" query string.",
|
||||||
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
|
"questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
|
||||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
"cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
|
||||||
"groupMemberNotFound": "User not found among group's members",
|
"onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
|
||||||
"mustBeGroupMember": "Must be member of the group.",
|
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||||
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
|
"groupMemberNotFound": "User not found among group's members",
|
||||||
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
|
"mustBeGroupMember": "Must be member of the group.",
|
||||||
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
|
"keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
|
||||||
"inviteMissingEmail": "Missing email address in invite.",
|
"keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
|
||||||
"inviteMissingUuid": "Missing user id in invite",
|
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
|
||||||
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
"inviteMissingEmail": "Missing email address in invite.",
|
||||||
"partyMustbePrivate": "Parties must be private",
|
"inviteMissingUuid": "Missing user id in invite",
|
||||||
"userAlreadyInGroup": "User already in that group.",
|
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
||||||
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
|
"partyMustbePrivate": "Parties must be private",
|
||||||
"userAlreadyInvitedToGroup": "User already invited to that group.",
|
"userAlreadyInGroup": "User already in that group.",
|
||||||
"userAlreadyPendingInvitation": "User already pending invitation.",
|
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
|
||||||
"userAlreadyInAParty": "User already in a party.",
|
"userAlreadyInvitedToGroup": "User already invited to that group.",
|
||||||
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
|
"userAlreadyPendingInvitation": "User already pending invitation.",
|
||||||
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
|
"userAlreadyInAParty": "User already in a party.",
|
||||||
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
|
||||||
"emailsMustBeAnArray": "Email address invites must be an array.",
|
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
|
||||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
||||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
"emailsMustBeAnArray": "Email address invites must be an array.",
|
||||||
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
||||||
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||||
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
|
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
|
||||||
"newChatMessageTitle": "New message in <%= groupName %>",
|
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
|
||||||
"exportInbox": "Export Messages",
|
"newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
|
||||||
"exportInboxPopoverTitle": "Export your messages as HTML",
|
"newChatMessageTitle": "New message in <%= groupName %>",
|
||||||
"exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
|
"exportInbox": "Export Messages",
|
||||||
"to": "To:",
|
"exportInboxPopoverTitle": "Export your messages as HTML",
|
||||||
"from": "From:",
|
"exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
|
||||||
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
"to": "To:",
|
||||||
"confirmAddTag": "Do you really want to add \"<%= tag %>\"?",
|
"from": "From:",
|
||||||
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
|
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
||||||
"assignTask": "Assign Task",
|
"confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
|
||||||
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
|
||||||
"claim": "Claim",
|
|
||||||
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
"groupHomeTitle": "Home",
|
||||||
"yourTaskHasBeenApproved": "Your task has been approved",
|
"assignTask": "Assign Task",
|
||||||
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
|
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
|
||||||
"confirmTaskApproval": "Are you sure you want to approve this task?",
|
"claim": "Claim",
|
||||||
"approve": "Approve",
|
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
||||||
"approvalTitle": "<%= text %> for user: <%= userName %>"
|
"yourTaskHasBeenApproved": "Your task \"<%= taskText %>\" has been approved",
|
||||||
}
|
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
|
||||||
|
"approve": "Approve",
|
||||||
|
"approvalTitle": "<%= text %> for user: <%= userName %>",
|
||||||
|
"confirmTaskApproval": "Do you want to reward <%= username %> for completing this task?",
|
||||||
|
"groupSubscriptionPrice": "$9 every month + $3 a month for every additional group member",
|
||||||
|
|
||||||
|
"groupBenefitsTitle": "How a group plan can help you",
|
||||||
|
"groupBenefitsDescription": "We’ve just launched the beta version of our group plans! Upgrading to a group plan unlocks some unique features to optimize the social side of Habitica.",
|
||||||
|
"groupBenefitOneTitle": "Create a shared task list",
|
||||||
|
"groupBenefitOneDescription": "Set up a shared task list for the group that everyone can easily view and edit.",
|
||||||
|
"groupBenefitTwoTitle": "Assign tasks to group members",
|
||||||
|
"groupBenefitTwoDescription": "Want a coworker to answer a critical email? Need your roommate to pick up the groceries? Just assign them the tasks you create, and they’ll automatically appear in that person’s task dashboard.",
|
||||||
|
"groupBenefitThreeTitle": "Claim a task that you are working on",
|
||||||
|
"groupBenefitThreeDescription": "Stake your claim on any group task with a simple click. Make it clear what everybody is working on!",
|
||||||
|
"groupBenefitFourTitle": "Mark tasks that require special approval",
|
||||||
|
"groupBenefitFourDescription": "Need to verify that a task really did get done before that user gets their rewards? Just adjust the approval settings for added control.",
|
||||||
|
"groupBenefitFiveTitle": "Chat privately with your group",
|
||||||
|
"groupBenefitFiveDescription": "Stay in the loop about important decisions in our easy-to-use chatroom!",
|
||||||
|
"createAGroup": "Create A Group",
|
||||||
|
|
||||||
|
"assignFieldPlaceholder": "Type a group member's profile name",
|
||||||
|
"cannotDeleteActiveGroup": "You cannot remove a group with an active subscription",
|
||||||
|
"groupTasksTitle": "Group Tasks List",
|
||||||
|
"approvalsTitle": "Tasks Awaiting Approval",
|
||||||
|
"upgradeTitle": "Upgrade",
|
||||||
|
"blankApprovalsDescription": "When your group completes tasks that need your approval, they’ll appear here! Adjust approval requirement settings under task editing.",
|
||||||
|
"userIsClamingTask": "<%= username %> has claimed \"<%= task %>\"",
|
||||||
|
"approvalRequested": "Approval Requested",
|
||||||
|
"refreshApprovals": "Refresh Approvals",
|
||||||
|
"refreshGroupTasks": "Refresh Group Tasks",
|
||||||
|
"claimedBy": "\n\nClaimed by: <%= claimingUsers %>",
|
||||||
|
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
|
||||||
|
"confirmGuildPlanCreation": "Create this group?"
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import gear from './gear';
|
|||||||
import appearances from './appearance';
|
import appearances from './appearance';
|
||||||
import backgrounds from './appearance/backgrounds.js'
|
import backgrounds from './appearance/backgrounds.js'
|
||||||
import spells from './spells';
|
import spells from './spells';
|
||||||
|
import subscriptionBlocks from './subscriptionBlocks';
|
||||||
import faq from './faq';
|
import faq from './faq';
|
||||||
import timeTravelers from './time-travelers';
|
import timeTravelers from './time-travelers';
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ api.itemList = ITEM_LIST;
|
|||||||
|
|
||||||
api.gear = gear;
|
api.gear = gear;
|
||||||
api.spells = spells;
|
api.spells = spells;
|
||||||
|
api.subscriptionBlocks = subscriptionBlocks;
|
||||||
|
|
||||||
api.mystery = timeTravelers.mystery;
|
api.mystery = timeTravelers.mystery;
|
||||||
api.timeTravelerStore = timeTravelers.timeTravelerStore;
|
api.timeTravelerStore = timeTravelers.timeTravelerStore;
|
||||||
@@ -2808,35 +2810,6 @@ api.appearances = appearances;
|
|||||||
|
|
||||||
api.backgrounds = backgrounds;
|
api.backgrounds = backgrounds;
|
||||||
|
|
||||||
api.subscriptionBlocks = {
|
|
||||||
basic_earned: {
|
|
||||||
months: 1,
|
|
||||||
price: 5
|
|
||||||
},
|
|
||||||
basic_3mo: {
|
|
||||||
months: 3,
|
|
||||||
price: 15
|
|
||||||
},
|
|
||||||
basic_6mo: {
|
|
||||||
months: 6,
|
|
||||||
price: 30
|
|
||||||
},
|
|
||||||
google_6mo: {
|
|
||||||
months: 6,
|
|
||||||
price: 24,
|
|
||||||
discount: true,
|
|
||||||
original: 30
|
|
||||||
},
|
|
||||||
basic_12mo: {
|
|
||||||
months: 12,
|
|
||||||
price: 48
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_.each(api.subscriptionBlocks, function(b, k) {
|
|
||||||
return b.key = k;
|
|
||||||
});
|
|
||||||
|
|
||||||
api.userDefaults = {
|
api.userDefaults = {
|
||||||
habits: [
|
habits: [
|
||||||
{
|
{
|
||||||
|
|||||||
39
website/common/script/content/subscriptionBlocks.js
Normal file
39
website/common/script/content/subscriptionBlocks.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
let subscriptionBlocks = {
|
||||||
|
basic_earned: {
|
||||||
|
months: 1,
|
||||||
|
price: 5,
|
||||||
|
},
|
||||||
|
basic_3mo: {
|
||||||
|
months: 3,
|
||||||
|
price: 15,
|
||||||
|
},
|
||||||
|
basic_6mo: {
|
||||||
|
months: 6,
|
||||||
|
price: 30,
|
||||||
|
},
|
||||||
|
google_6mo: {
|
||||||
|
months: 6,
|
||||||
|
price: 24,
|
||||||
|
discount: true,
|
||||||
|
original: 30,
|
||||||
|
},
|
||||||
|
basic_12mo: {
|
||||||
|
months: 12,
|
||||||
|
price: 48,
|
||||||
|
},
|
||||||
|
group_monthly: {
|
||||||
|
type: 'group',
|
||||||
|
months: 1,
|
||||||
|
price: 9,
|
||||||
|
quantity: 3, // Default quantity for Stripe - The same as having 3 user subscriptions
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(subscriptionBlocks, function createKeys (b, k) {
|
||||||
|
return b.key = k;
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = subscriptionBlocks;
|
||||||
@@ -179,6 +179,8 @@ module.exports = function scoreTask (options = {}, req = {}) {
|
|||||||
exp: user.stats.exp,
|
exp: user.stats.exp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (task.group && task.group.approval && task.group.approval.required && !task.group.approval.approved) return;
|
||||||
|
|
||||||
// This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
|
// This is for setting one-time temporary flags, such as streakBonus or itemDropped. Useful for notifying
|
||||||
// the API consumer, then cleared afterwards
|
// the API consumer, then cleared afterwards
|
||||||
user._tmp = {};
|
user._tmp = {};
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import { encrypt } from '../../libs/encryption';
|
|||||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||||
import pusher from '../../libs/pusher';
|
import pusher from '../../libs/pusher';
|
||||||
import common from '../../../common';
|
import common from '../../../common';
|
||||||
|
import payments from '../../libs/payments';
|
||||||
|
import shared from '../../../common';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine GroupBodyInvalid
|
* @apiDefine GroupBodyInvalid
|
||||||
@@ -106,6 +109,95 @@ api.createGroup = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {post} /api/v3/groups/create-plan Create a Group and then redirect to the correct payment
|
||||||
|
* @apiName CreateGroupPlan
|
||||||
|
* @apiGroup Group
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data The created group
|
||||||
|
*/
|
||||||
|
api.createGroupPlan = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/groups/create-plan',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
let group = new Group(Group.sanitize(req.body.groupToCreate));
|
||||||
|
|
||||||
|
req.checkBody('paymentType', res.t('paymentTypeRequired')).notEmpty();
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
// @TODO: Change message
|
||||||
|
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
|
||||||
|
group.leader = user._id;
|
||||||
|
user.guilds.push(group._id);
|
||||||
|
|
||||||
|
let results = await Bluebird.all([user.save(), group.save()]);
|
||||||
|
let savedGroup = results[1];
|
||||||
|
|
||||||
|
// Analytics
|
||||||
|
let analyticsObject = {
|
||||||
|
uuid: user._id,
|
||||||
|
hitType: 'event',
|
||||||
|
category: 'behavior',
|
||||||
|
owner: true,
|
||||||
|
groupType: savedGroup.type,
|
||||||
|
privacy: savedGroup.privacy,
|
||||||
|
headers: req.headers,
|
||||||
|
};
|
||||||
|
res.analytics.track('join group', analyticsObject);
|
||||||
|
|
||||||
|
if (req.body.paymentType === 'Stripe') {
|
||||||
|
let token = req.body.id;
|
||||||
|
let gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||||
|
let sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
||||||
|
let groupId = savedGroup._id;
|
||||||
|
let email = req.body.email;
|
||||||
|
let headers = req.headers;
|
||||||
|
let coupon = req.query.coupon;
|
||||||
|
|
||||||
|
await payments.payWithStripe([
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
gift,
|
||||||
|
sub,
|
||||||
|
groupId,
|
||||||
|
email,
|
||||||
|
headers,
|
||||||
|
coupon,
|
||||||
|
]);
|
||||||
|
} else if (req.body.paymentType === 'Amazon') {
|
||||||
|
let billingAgreementId = req.body.billingAgreementId;
|
||||||
|
let sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
||||||
|
let coupon = req.body.coupon;
|
||||||
|
let groupId = savedGroup._id;
|
||||||
|
let headers = req.headers;
|
||||||
|
|
||||||
|
await payments.subscribeWithAmazon([
|
||||||
|
billingAgreementId,
|
||||||
|
sub,
|
||||||
|
coupon,
|
||||||
|
user,
|
||||||
|
groupId,
|
||||||
|
headers,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||||
|
// await Q.ninvoke(savedGroup, 'populate', ['leader', nameFields]); // doc.populate doesn't return a promise
|
||||||
|
let response = savedGroup.toJSON();
|
||||||
|
// the leader is the authenticated user
|
||||||
|
response.leader = {
|
||||||
|
_id: user._id,
|
||||||
|
profile: {name: user.profile.name},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.respond(201, response); // do not remove chat flags data as we've just created the group
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} /api/v3/groups Get groups for a user
|
* @api {get} /api/v3/groups Get groups for a user
|
||||||
* @apiName GetGroups
|
* @apiName GetGroups
|
||||||
@@ -303,6 +395,8 @@ api.joinGroup = {
|
|||||||
|
|
||||||
group.memberCount += 1;
|
group.memberCount += 1;
|
||||||
|
|
||||||
|
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||||
|
|
||||||
let promises = [group.save(), user.save()];
|
let promises = [group.save(), user.save()];
|
||||||
|
|
||||||
if (inviter) {
|
if (inviter) {
|
||||||
@@ -459,6 +553,8 @@ api.leaveGroup = {
|
|||||||
|
|
||||||
await group.leave(user, req.query.keep);
|
await group.leave(user, req.query.keep);
|
||||||
|
|
||||||
|
if (group.purchased.plan && group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||||
|
|
||||||
_removeMessagesFromMember(user, group._id);
|
_removeMessagesFromMember(user, group._id);
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
@@ -535,6 +631,7 @@ api.removeGroupMember = {
|
|||||||
|
|
||||||
if (isInGroup) {
|
if (isInGroup) {
|
||||||
group.memberCount -= 1;
|
group.memberCount -= 1;
|
||||||
|
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||||
|
|
||||||
if (group.quest && group.quest.leader === member._id) {
|
if (group.quest && group.quest.leader === member._id) {
|
||||||
group.quest.key = undefined;
|
group.quest.key = undefined;
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ api.updateTask = {
|
|||||||
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
|
} else if (task.userId !== user._id) { // If the task is owned by a user make it's the current one
|
||||||
throw new NotFound(res.t('taskNotFound'));
|
throw new NotFound(res.t('taskNotFound'));
|
||||||
}
|
}
|
||||||
|
let oldCheckList = task.checklist;
|
||||||
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
|
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
|
||||||
let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
|
let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
|
||||||
|
|
||||||
@@ -254,6 +254,8 @@ api.updateTask = {
|
|||||||
|
|
||||||
if (!challenge && task.userId && task.challenge && task.challenge.id) {
|
if (!challenge && task.userId && task.challenge && task.challenge.id) {
|
||||||
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
|
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
|
||||||
|
} else if (!group && task.userId && task.group && task.group.id) {
|
||||||
|
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
|
||||||
} else {
|
} else {
|
||||||
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
|
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
|
||||||
}
|
}
|
||||||
@@ -270,7 +272,15 @@ api.updateTask = {
|
|||||||
let savedTask = await task.save();
|
let savedTask = await task.save();
|
||||||
|
|
||||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||||
await group.updateTask(savedTask);
|
let updateCheckListItems = _.remove(sanitizedObj.checklist, function getCheckListsToUpdate (checklist) {
|
||||||
|
let indexOld = _.findIndex(oldCheckList, function findIndex (check) {
|
||||||
|
return check.id === checklist.id;
|
||||||
|
});
|
||||||
|
if (indexOld !== -1) return checklist.text !== oldCheckList[indexOld].text;
|
||||||
|
return false; // Only return changes. Adding and remove are handled differently
|
||||||
|
});
|
||||||
|
|
||||||
|
await group.updateTask(savedTask, {updateCheckListItems});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.respond(200, savedTask);
|
res.respond(200, savedTask);
|
||||||
@@ -508,13 +518,16 @@ api.addChecklistItem = {
|
|||||||
|
|
||||||
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
if (task.type !== 'daily' && task.type !== 'todo') throw new BadRequest(res.t('checklistOnlyDailyTodo'));
|
||||||
|
|
||||||
task.checklist.push(Tasks.Task.sanitizeChecklist(req.body));
|
let newCheckListItem = Tasks.Task.sanitizeChecklist(req.body);
|
||||||
|
task.checklist.push(newCheckListItem);
|
||||||
let savedTask = await task.save();
|
let savedTask = await task.save();
|
||||||
|
|
||||||
|
newCheckListItem.id = savedTask.checklist[savedTask.checklist.length - 1].id;
|
||||||
|
|
||||||
res.respond(200, savedTask);
|
res.respond(200, savedTask);
|
||||||
if (challenge) challenge.updateTask(savedTask);
|
if (challenge) challenge.updateTask(savedTask);
|
||||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||||
await group.updateTask(savedTask);
|
await group.updateTask(savedTask, {newCheckListItem});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -676,7 +689,7 @@ api.removeChecklistItem = {
|
|||||||
res.respond(200, savedTask);
|
res.respond(200, savedTask);
|
||||||
if (challenge) challenge.updateTask(savedTask);
|
if (challenge) challenge.updateTask(savedTask);
|
||||||
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||||
await group.updateTask(savedTask);
|
await group.updateTask(savedTask, {removedCheckListItemId: req.params.itemId});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -941,6 +954,8 @@ api.deleteTask = {
|
|||||||
throw new NotFound(res.t('taskNotFound'));
|
throw new NotFound(res.t('taskNotFound'));
|
||||||
} else if (task.userId && task.challenge.id && !task.challenge.broken) {
|
} else if (task.userId && task.challenge.id && !task.challenge.broken) {
|
||||||
throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
|
throw new NotAuthorized(res.t('cantDeleteChallengeTasks'));
|
||||||
|
} else if (task.group.id && task.group.assignedUsers.indexOf(user._id) !== -1) {
|
||||||
|
throw new NotAuthorized(res.t('cantDeleteAssignedGroupTasks'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.type !== 'todo' || !task.completed) {
|
if (task.type !== 'todo' || !task.completed) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { authWithHeaders } from '../../../middlewares/auth';
|
import { authWithHeaders } from '../../../middlewares/auth';
|
||||||
import ensureDevelpmentMode from '../../../middlewares/ensureDevelpmentMode';
|
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import * as Tasks from '../../../models/task';
|
import * as Tasks from '../../../models/task';
|
||||||
import { model as Group } from '../../../models/group';
|
import { model as Group } from '../../../models/group';
|
||||||
@@ -22,7 +21,6 @@ let api = {};
|
|||||||
* @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
* @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks.
|
||||||
* @apiName CreateGroupTasks
|
* @apiName CreateGroupTasks
|
||||||
* @apiGroup Task
|
* @apiGroup Task
|
||||||
* @apiIgnore
|
|
||||||
*
|
*
|
||||||
* @apiParam {UUID} groupId The id of the group the new task(s) will belong to
|
* @apiParam {UUID} groupId The id of the group the new task(s) will belong to
|
||||||
*
|
*
|
||||||
@@ -31,7 +29,7 @@ let api = {};
|
|||||||
api.createGroupTasks = {
|
api.createGroupTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/group/:groupId',
|
url: '/tasks/group/:groupId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ api.createGroupTasks = {
|
|||||||
* @api {get} /api/v3/tasks/group/:groupId Get a group's tasks
|
* @api {get} /api/v3/tasks/group/:groupId Get a group's tasks
|
||||||
* @apiName GetGroupTasks
|
* @apiName GetGroupTasks
|
||||||
* @apiGroup Task
|
* @apiGroup Task
|
||||||
* @apiIgnore
|
|
||||||
*
|
*
|
||||||
* @apiParam {UUID} groupId The id of the group from which to retrieve the tasks
|
* @apiParam {UUID} groupId The id of the group from which to retrieve the tasks
|
||||||
* @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks
|
* @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks
|
||||||
@@ -65,7 +62,7 @@ api.createGroupTasks = {
|
|||||||
api.getGroupTasks = {
|
api.getGroupTasks = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/tasks/group/:groupId',
|
url: '/tasks/group/:groupId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||||
@@ -97,7 +94,7 @@ api.getGroupTasks = {
|
|||||||
api.assignTask = {
|
api.assignTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -120,12 +117,22 @@ api.assignTask = {
|
|||||||
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
let groupFields = `${requiredGroupFields} chat`;
|
||||||
|
let group = await Group.getGroup({user, groupId: task.group.id, fields: groupFields});
|
||||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
if (group.leader !== user._id && user._id !== assignedUserId) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
if (group.leader !== user._id && user._id !== assignedUserId) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||||
|
|
||||||
await group.syncTask(task, assignedUser);
|
// User is claiming the task
|
||||||
|
if (user._id === assignedUserId) {
|
||||||
|
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
|
||||||
|
group.sendChat(message, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let promises = [];
|
||||||
|
promises.push(group.syncTask(task, assignedUser));
|
||||||
|
promises.push(group.save());
|
||||||
|
await Bluebird.all(promises);
|
||||||
|
|
||||||
res.respond(200, task);
|
res.respond(200, task);
|
||||||
},
|
},
|
||||||
@@ -145,7 +152,7 @@ api.assignTask = {
|
|||||||
api.unassignTask = {
|
api.unassignTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -194,7 +201,7 @@ api.unassignTask = {
|
|||||||
api.approveTask = {
|
api.approveTask = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/:taskId/approve/:userId',
|
url: '/tasks/:taskId/approve/:userId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||||
@@ -226,10 +233,15 @@ api.approveTask = {
|
|||||||
task.group.approval.approved = true;
|
task.group.approval.approved = true;
|
||||||
|
|
||||||
assignedUser.addNotification('GROUP_TASK_APPROVED', {
|
assignedUser.addNotification('GROUP_TASK_APPROVED', {
|
||||||
message: res.t('yourTaskHasBeenApproved'),
|
message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
|
||||||
groupId: group._id,
|
groupId: group._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
assignedUser.addNotification('SCORED_TASK', {
|
||||||
|
message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
|
||||||
|
scoreTask: task,
|
||||||
|
});
|
||||||
|
|
||||||
await Bluebird.all([assignedUser.save(), task.save()]);
|
await Bluebird.all([assignedUser.save(), task.save()]);
|
||||||
|
|
||||||
res.respond(200, task);
|
res.respond(200, task);
|
||||||
@@ -241,7 +253,6 @@ api.approveTask = {
|
|||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
* @apiName GetGroupApprovals
|
* @apiName GetGroupApprovals
|
||||||
* @apiGroup Task
|
* @apiGroup Task
|
||||||
* @apiIgnore
|
|
||||||
*
|
*
|
||||||
* @apiParam {UUID} groupId The id of the group from which to retrieve the approvals
|
* @apiParam {UUID} groupId The id of the group from which to retrieve the approvals
|
||||||
*
|
*
|
||||||
@@ -250,7 +261,7 @@ api.approveTask = {
|
|||||||
api.getGroupApprovals = {
|
api.getGroupApprovals = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/approvals/group/:groupId',
|
url: '/approvals/group/:groupId',
|
||||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ api.checkout = {
|
|||||||
let orderReferenceId = req.body.orderReferenceId;
|
let orderReferenceId = req.body.orderReferenceId;
|
||||||
let amount = 5;
|
let amount = 5;
|
||||||
|
|
||||||
|
// @TODO: Make thise use payment.subscribeWithAmazon
|
||||||
|
|
||||||
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
|
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
|
||||||
|
|
||||||
if (gift) {
|
if (gift) {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ api.checkout = {
|
|||||||
let groupId = req.query.groupId;
|
let groupId = req.query.groupId;
|
||||||
let coupon;
|
let coupon;
|
||||||
let response;
|
let response;
|
||||||
|
let subscriptionId;
|
||||||
|
|
||||||
|
// @TODO: Update this to use payments.payWithStripe
|
||||||
|
|
||||||
if (!token) throw new BadRequest('Missing req.body.id');
|
if (!token) throw new BadRequest('Missing req.body.id');
|
||||||
|
|
||||||
@@ -59,12 +62,20 @@ api.checkout = {
|
|||||||
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
||||||
}
|
}
|
||||||
|
|
||||||
response = await stripe.customers.create({
|
let customerObject = {
|
||||||
email: req.body.email,
|
email: req.body.email,
|
||||||
metadata: { uuid: user._id },
|
metadata: { uuid: user._id },
|
||||||
card: token,
|
card: token,
|
||||||
plan: sub.key,
|
plan: sub.key,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
customerObject.quantity = sub.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await stripe.customers.create(customerObject);
|
||||||
|
|
||||||
|
if (groupId) subscriptionId = response.subscriptions.data[0].id;
|
||||||
} else {
|
} else {
|
||||||
let amount = 500; // $5
|
let amount = 500; // $5
|
||||||
|
|
||||||
@@ -91,6 +102,7 @@ api.checkout = {
|
|||||||
sub,
|
sub,
|
||||||
headers: req.headers,
|
headers: req.headers,
|
||||||
groupId,
|
groupId,
|
||||||
|
subscriptionId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let method = 'buyGems';
|
let method = 'buyGems';
|
||||||
@@ -149,7 +161,9 @@ api.subscribeEdit = {
|
|||||||
throw new NotFound(res.t('groupNotFound'));
|
throw new NotFound(res.t('groupNotFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!group.leader === user._id) {
|
let allowedManagers = [group.leader, group.purchased.plan.owner];
|
||||||
|
|
||||||
|
if (allowedManagers.indexOf(user._id) === -1) {
|
||||||
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
throw new NotAuthorized(res.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
}
|
}
|
||||||
customerId = group.purchased.plan.customerId;
|
customerId = group.purchased.plan.customerId;
|
||||||
|
|||||||
@@ -11,11 +11,23 @@ import {
|
|||||||
model as Group,
|
model as Group,
|
||||||
basicFields as basicGroupFields,
|
basicFields as basicGroupFields,
|
||||||
} from '../models/group';
|
} from '../models/group';
|
||||||
|
import { model as Coupon } from '../models/coupon';
|
||||||
|
import { model as User } from '../models/user';
|
||||||
import {
|
import {
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
NotFound,
|
NotFound,
|
||||||
} from './errors';
|
} from './errors';
|
||||||
import slack from './slack';
|
import slack from './slack';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import stripeModule from 'stripe';
|
||||||
|
import amzLib from './amazonPayments';
|
||||||
|
import {
|
||||||
|
BadRequest,
|
||||||
|
} from './errors';
|
||||||
|
import cc from 'coupon-code';
|
||||||
|
|
||||||
|
const stripe = stripeModule(nconf.get('STRIPE_API_KEY'));
|
||||||
|
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -49,6 +61,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
let groupId;
|
let groupId;
|
||||||
let itemPurchased = 'Subscription';
|
let itemPurchased = 'Subscription';
|
||||||
let purchaseType = 'subscribe';
|
let purchaseType = 'subscribe';
|
||||||
|
let emailType = 'subscription-begins';
|
||||||
|
|
||||||
// If we are buying a group subscription
|
// If we are buying a group subscription
|
||||||
if (data.groupId) {
|
if (data.groupId) {
|
||||||
@@ -66,7 +79,9 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
recipient = group;
|
recipient = group;
|
||||||
itemPurchased = 'Group-Subscription';
|
itemPurchased = 'Group-Subscription';
|
||||||
purchaseType = 'group-subscribe';
|
purchaseType = 'group-subscribe';
|
||||||
|
emailType = 'group-subscription-begins';
|
||||||
groupId = group._id;
|
groupId = group._id;
|
||||||
|
recipient.purchased.plan.quantity = data.sub.quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
plan = recipient.purchased.plan;
|
plan = recipient.purchased.plan;
|
||||||
@@ -98,11 +113,16 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
// Specify a lastBillingDate just for Amazon Payments
|
// Specify a lastBillingDate just for Amazon Payments
|
||||||
// Resetted every time the subscription restarts
|
// Resetted every time the subscription restarts
|
||||||
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined,
|
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined,
|
||||||
|
owner: data.user._id,
|
||||||
}).defaults({ // allow non-override if a plan was previously used
|
}).defaults({ // allow non-override if a plan was previously used
|
||||||
gemsBought: 0,
|
gemsBought: 0,
|
||||||
dateCreated: today,
|
dateCreated: today,
|
||||||
mysteryItems: [],
|
mysteryItems: [],
|
||||||
}).value();
|
}).value();
|
||||||
|
|
||||||
|
if (data.subscriptionId) {
|
||||||
|
plan.subscriptionId = data.subscriptionId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Block sub perks
|
// Block sub perks
|
||||||
@@ -119,7 +139,7 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data.gift) {
|
if (!data.gift) {
|
||||||
txnEmail(data.user, 'subscription-begins');
|
txnEmail(data.user, emailType);
|
||||||
}
|
}
|
||||||
|
|
||||||
analytics.trackPurchase({
|
analytics.trackPurchase({
|
||||||
@@ -208,12 +228,29 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.updateStripeGroupPlan = async function updateStripeGroupPlan (group, stripeInc) {
|
||||||
|
if (group.purchased.plan.paymentMethod !== 'Stripe') return;
|
||||||
|
let stripeApi = stripeInc || stripe;
|
||||||
|
let plan = shared.content.subscriptionBlocks.group_monthly;
|
||||||
|
|
||||||
|
await stripeApi.subscriptions.update(
|
||||||
|
group.purchased.plan.subscriptionId,
|
||||||
|
{
|
||||||
|
plan: plan.key,
|
||||||
|
quantity: group.memberCount + plan.quantity - 1,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
group.purchased.plan.quantity = group.memberCount + plan.quantity - 1;
|
||||||
|
};
|
||||||
|
|
||||||
// Sets their subscription to be cancelled later
|
// Sets their subscription to be cancelled later
|
||||||
api.cancelSubscription = async function cancelSubscription (data) {
|
api.cancelSubscription = async function cancelSubscription (data) {
|
||||||
let plan;
|
let plan;
|
||||||
let group;
|
let group;
|
||||||
let cancelType = 'unsubscribe';
|
let cancelType = 'unsubscribe';
|
||||||
let groupId;
|
let groupId;
|
||||||
|
let emailType = 'cancel-subscription';
|
||||||
|
|
||||||
// If we are buying a group subscription
|
// If we are buying a group subscription
|
||||||
if (data.groupId) {
|
if (data.groupId) {
|
||||||
@@ -224,10 +261,13 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
|||||||
throw new NotFound(shared.i18n.t('groupNotFound'));
|
throw new NotFound(shared.i18n.t('groupNotFound'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!group.leader === data.user._id) {
|
let allowedManagers = [group.leader, group.purchased.plan.owner];
|
||||||
|
|
||||||
|
if (allowedManagers.indexOf(data.user._id) === -1) {
|
||||||
throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription'));
|
throw new NotAuthorized(shared.i18n.t('onlyGroupLeaderCanManageSubscription'));
|
||||||
}
|
}
|
||||||
plan = group.purchased.plan;
|
plan = group.purchased.plan;
|
||||||
|
emailType = 'group-cancel-subscription';
|
||||||
} else {
|
} else {
|
||||||
plan = data.user.purchased.plan;
|
plan = data.user.purchased.plan;
|
||||||
}
|
}
|
||||||
@@ -252,7 +292,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
|||||||
await data.user.save();
|
await data.user.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
txnEmail(data.user, 'cancel-subscription');
|
txnEmail(data.user, emailType);
|
||||||
|
|
||||||
if (group) {
|
if (group) {
|
||||||
cancelType = 'group-unsubscribe';
|
cancelType = 'group-unsubscribe';
|
||||||
@@ -343,4 +383,180 @@ api.buyGems = async function buyGems (data) {
|
|||||||
await data.user.save();
|
await data.user.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows for purchasing a user subscription, group subscription or gems with Stripe
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @param options.token The stripe token generated on the front end
|
||||||
|
* @param options.user The user object who is purchasing
|
||||||
|
* @param options.gift The gift details if any
|
||||||
|
* @param options.sub The subscription data to purchase
|
||||||
|
* @param options.groupId The id of the group purchasing a subscription
|
||||||
|
* @param options.email The email enter by the user on the Stripe form
|
||||||
|
* @param options.headers The request headers to store on analytics
|
||||||
|
* @return undefined
|
||||||
|
*/
|
||||||
|
api.payWithStripe = async function payWithStripe (options, stripeInc) {
|
||||||
|
let [
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
gift,
|
||||||
|
sub,
|
||||||
|
groupId,
|
||||||
|
email,
|
||||||
|
headers,
|
||||||
|
coupon,
|
||||||
|
] = options;
|
||||||
|
let response;
|
||||||
|
let subscriptionId;
|
||||||
|
// @TODO: We need to mock this, but curently we don't have correct Dependency Injection
|
||||||
|
let stripeApi = stripe;
|
||||||
|
|
||||||
|
if (stripeInc) stripeApi = stripeInc;
|
||||||
|
|
||||||
|
if (!token) throw new BadRequest('Missing req.body.id');
|
||||||
|
|
||||||
|
if (sub) {
|
||||||
|
if (sub.discount) {
|
||||||
|
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
|
||||||
|
coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
|
||||||
|
if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let customerObject = {
|
||||||
|
email,
|
||||||
|
metadata: { uuid: user._id },
|
||||||
|
card: token,
|
||||||
|
plan: sub.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
customerObject.quantity = sub.quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await stripeApi.customers.create(customerObject);
|
||||||
|
|
||||||
|
if (groupId) subscriptionId = response.subscriptions.data[0].id;
|
||||||
|
} else {
|
||||||
|
let amount = 500; // $5
|
||||||
|
|
||||||
|
if (gift) {
|
||||||
|
if (gift.type === 'subscription') {
|
||||||
|
amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`;
|
||||||
|
} else {
|
||||||
|
amount = `${gift.gems.amount / 4 * 100}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response = await stripe.charges.create({
|
||||||
|
amount,
|
||||||
|
currency: 'usd',
|
||||||
|
card: token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub) {
|
||||||
|
await this.createSubscription({
|
||||||
|
user,
|
||||||
|
customerId: response.id,
|
||||||
|
paymentMethod: 'Stripe',
|
||||||
|
sub,
|
||||||
|
headers,
|
||||||
|
groupId,
|
||||||
|
subscriptionId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let method = 'buyGems';
|
||||||
|
let data = {
|
||||||
|
user,
|
||||||
|
customerId: response.id,
|
||||||
|
paymentMethod: 'Stripe',
|
||||||
|
gift,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (gift) {
|
||||||
|
let member = await User.findById(gift.uuid).exec();
|
||||||
|
gift.member = member;
|
||||||
|
if (gift.type === 'subscription') method = 'createSubscription';
|
||||||
|
data.paymentMethod = 'Gift';
|
||||||
|
}
|
||||||
|
|
||||||
|
await this[method](data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows for purchasing a user subscription or group subscription with Amazon
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* @param options.billingAgreementId The Amazon billingAgreementId generated on the front end
|
||||||
|
* @param options.user The user object who is purchasing
|
||||||
|
* @param options.sub The subscription data to purchase
|
||||||
|
* @param options.coupon The coupon to discount the sub
|
||||||
|
* @param options.groupId The id of the group purchasing a subscription
|
||||||
|
* @param options.headers The request headers to store on analytics
|
||||||
|
* @return undefined
|
||||||
|
*/
|
||||||
|
api.subscribeWithAmazon = async function subscribeWithAmazon (options) {
|
||||||
|
let [
|
||||||
|
billingAgreementId,
|
||||||
|
sub,
|
||||||
|
coupon,
|
||||||
|
user,
|
||||||
|
groupId,
|
||||||
|
headers,
|
||||||
|
] = options;
|
||||||
|
|
||||||
|
if (!sub) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||||
|
if (!billingAgreementId) throw new BadRequest('Missing req.body.billingAgreementId');
|
||||||
|
|
||||||
|
if (sub.discount) { // apply discount
|
||||||
|
if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired'));
|
||||||
|
let result = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec();
|
||||||
|
if (!result) throw new NotAuthorized(shared.i18n.t('invalidCoupon'));
|
||||||
|
}
|
||||||
|
|
||||||
|
await amzLib.setBillingAgreementDetails({
|
||||||
|
AmazonBillingAgreementId: billingAgreementId,
|
||||||
|
BillingAgreementAttributes: {
|
||||||
|
SellerNote: 'Habitica Subscription',
|
||||||
|
SellerBillingAgreementAttributes: {
|
||||||
|
SellerBillingAgreementId: shared.uuid(),
|
||||||
|
StoreName: 'Habitica',
|
||||||
|
CustomInformation: 'Habitica Subscription',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await amzLib.confirmBillingAgreement({
|
||||||
|
AmazonBillingAgreementId: billingAgreementId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await amzLib.authorizeOnBillingAgreement({
|
||||||
|
AmazonBillingAgreementId: billingAgreementId,
|
||||||
|
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||||
|
AuthorizationAmount: {
|
||||||
|
CurrencyCode: 'USD',
|
||||||
|
Amount: sub.price,
|
||||||
|
},
|
||||||
|
SellerAuthorizationNote: 'Habitica Subscription Payment',
|
||||||
|
TransactionTimeout: 0,
|
||||||
|
CaptureNow: true,
|
||||||
|
SellerNote: 'Habitica Subscription Payment',
|
||||||
|
SellerOrderAttributes: {
|
||||||
|
SellerOrderId: shared.uuid(),
|
||||||
|
StoreName: 'Habitica',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.createSubscription({
|
||||||
|
user,
|
||||||
|
customerId: billingAgreementId,
|
||||||
|
paymentMethod: 'Amazon Payments',
|
||||||
|
sub,
|
||||||
|
headers,
|
||||||
|
groupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ async function cronAsync (req, res) {
|
|||||||
|
|
||||||
// Clear old completed todos - 30 days for free users, 90 for subscribers
|
// Clear old completed todos - 30 days for free users, 90 for subscribers
|
||||||
// Do not delete challenges completed todos TODO unless the task is broken?
|
// Do not delete challenges completed todos TODO unless the task is broken?
|
||||||
|
// Do not delete group completed todos
|
||||||
Tasks.Task.remove({
|
Tasks.Task.remove({
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
type: 'todo',
|
type: 'todo',
|
||||||
@@ -145,6 +146,7 @@ async function cronAsync (req, res) {
|
|||||||
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
|
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
|
||||||
},
|
},
|
||||||
'challenge.id': {$exists: false},
|
'challenge.id': {$exists: false},
|
||||||
|
'group.id': {$exists: false},
|
||||||
}).exec();
|
}).exec();
|
||||||
|
|
||||||
res.locals.wasModified = true; // TODO remove after v2 is retired
|
res.locals.wasModified = true; // TODO remove after v2 is retired
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { groupChatReceivedWebhook } from '../libs/webhook';
|
|||||||
import {
|
import {
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
} from '../libs/errors';
|
} from '../libs/errors';
|
||||||
import baseModel from '../libs/baseModel';
|
import baseModel from '../libs/baseModel';
|
||||||
import { sendTxn as sendTxnEmail } from '../libs/email';
|
import { sendTxn as sendTxnEmail } from '../libs/email';
|
||||||
@@ -859,6 +860,11 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
|||||||
let group = this;
|
let group = this;
|
||||||
let update = {};
|
let update = {};
|
||||||
|
|
||||||
|
let plan = group.purchased.plan;
|
||||||
|
if (group.memberCount <= 1 && group.privacy === 'private' && plan && plan.customerId && !plan.dateTerminated) {
|
||||||
|
throw new NotAuthorized(shared.i18n.t('cannotDeleteActiveGroup'));
|
||||||
|
}
|
||||||
|
|
||||||
let challenges = await Challenge.find({
|
let challenges = await Challenge.find({
|
||||||
_id: {$in: user.challenges},
|
_id: {$in: user.challenges},
|
||||||
group: group._id,
|
group: group._id,
|
||||||
@@ -899,6 +905,13 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
|||||||
promises.push(group.remove());
|
promises.push(group.remove());
|
||||||
return await Bluebird.all(promises);
|
return await Bluebird.all(promises);
|
||||||
}
|
}
|
||||||
|
} else if (group.leader === user._id) { // otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
||||||
|
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
|
||||||
|
query._id = {$ne: user._id};
|
||||||
|
let seniorMember = await User.findOne(query).select('_id').exec();
|
||||||
|
|
||||||
|
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
|
||||||
|
if (seniorMember) update.$set = {leader: seniorMember._id};
|
||||||
}
|
}
|
||||||
// otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
// otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
||||||
update.$inc = {memberCount: -1};
|
update.$inc = {memberCount: -1};
|
||||||
@@ -915,7 +928,16 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
|||||||
return await Bluebird.all(promises);
|
return await Bluebird.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.updateTask = async function updateTask (taskToSync) {
|
/**
|
||||||
|
* Updates all linked tasks for a group task
|
||||||
|
*
|
||||||
|
* @param taskToSync The group task that will be synced
|
||||||
|
* @param options.newCheckListItem The new checklist item that needs to be synced to all assigned users
|
||||||
|
* @param options.removedCheckListItem The removed checklist item that needs to be removed from all assigned users
|
||||||
|
*
|
||||||
|
* @return The created tasks
|
||||||
|
*/
|
||||||
|
schema.methods.updateTask = async function updateTask (taskToSync, options = {}) {
|
||||||
let group = this;
|
let group = this;
|
||||||
|
|
||||||
let updateCmd = {$set: {}};
|
let updateCmd = {$set: {}};
|
||||||
@@ -926,14 +948,51 @@ schema.methods.updateTask = async function updateTask (taskToSync) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
|
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
|
||||||
|
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
|
||||||
|
|
||||||
let taskSchema = Tasks[taskToSync.type];
|
let taskSchema = Tasks[taskToSync.type];
|
||||||
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
|
|
||||||
await taskSchema.update({
|
let updateQuery = {
|
||||||
userId: {$exists: true},
|
userId: {$exists: true},
|
||||||
'group.id': group.id,
|
'group.id': group.id,
|
||||||
'group.taskId': taskToSync._id,
|
'group.taskId': taskToSync._id,
|
||||||
}, updateCmd, {multi: true}).exec();
|
};
|
||||||
|
|
||||||
|
if (options.newCheckListItem) {
|
||||||
|
let newCheckList = {completed: false};
|
||||||
|
newCheckList.linkId = options.newCheckListItem.id;
|
||||||
|
newCheckList.text = options.newCheckListItem.text;
|
||||||
|
updateCmd.$push = { checklist: newCheckList };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.removedCheckListItemId) {
|
||||||
|
updateCmd.$pull = { checklist: {linkId: {$in: [options.removedCheckListItemId]} } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.updateCheckListItems && options.updateCheckListItems.length > 0) {
|
||||||
|
let checkListIdsToRemove = [];
|
||||||
|
let checkListItemsToAdd = [];
|
||||||
|
|
||||||
|
options.updateCheckListItems.forEach(function gatherChecklists (updateCheckListItem) {
|
||||||
|
checkListIdsToRemove.push(updateCheckListItem.id);
|
||||||
|
let newCheckList = {completed: false};
|
||||||
|
newCheckList.linkId = updateCheckListItem.id;
|
||||||
|
newCheckList.text = updateCheckListItem.text;
|
||||||
|
checkListItemsToAdd.push(newCheckList);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCmd.$pull = { checklist: {linkId: {$in: checkListIdsToRemove} } };
|
||||||
|
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
|
||||||
|
|
||||||
|
delete updateCmd.$pull;
|
||||||
|
updateCmd.$push = { checklist: { $each: checkListItemsToAdd } };
|
||||||
|
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks
|
||||||
|
await taskSchema.update(updateQuery, updateCmd, {multi: true}).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
||||||
@@ -952,11 +1011,13 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
|||||||
if (userTags[i].name !== group.name) {
|
if (userTags[i].name !== group.name) {
|
||||||
// update the name - it's been changed since
|
// update the name - it's been changed since
|
||||||
userTags[i].name = group.name;
|
userTags[i].name = group.name;
|
||||||
|
userTags[i].group = group._id;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
userTags.push({
|
userTags.push({
|
||||||
id: group._id,
|
id: group._id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
|
group: group._id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,6 +1043,17 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
matchingTask.group.approval.required = taskToSync.group.approval.required;
|
matchingTask.group.approval.required = taskToSync.group.approval.required;
|
||||||
|
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
|
||||||
|
|
||||||
|
// sync checklist
|
||||||
|
if (taskToSync.checklist) {
|
||||||
|
taskToSync.checklist.forEach(function syncCheckList (element) {
|
||||||
|
let newCheckList = {completed: false};
|
||||||
|
newCheckList.linkId = element.id;
|
||||||
|
newCheckList.text = element.text;
|
||||||
|
matchingTask.checklist.push(newCheckList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided
|
if (!matchingTask.notes) matchingTask.notes = taskToSync.notes; // don't override the notes, but provide it if not provided
|
||||||
if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing
|
if (matchingTask.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import baseModel from '../libs/baseModel';
|
import baseModel from '../libs/baseModel';
|
||||||
|
import validator from 'validator';
|
||||||
|
|
||||||
export let schema = new mongoose.Schema({
|
export let schema = new mongoose.Schema({
|
||||||
planId: String,
|
planId: String,
|
||||||
|
subscriptionId: String,
|
||||||
|
owner: {type: String, ref: 'User', validate: [validator.isUUID, 'Invalid uuid.']},
|
||||||
|
quantity: {type: Number, default: 1},
|
||||||
paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
|
paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']}
|
||||||
customerId: String, // Billing Agreement Id in case of Amazon Payments
|
customerId: String, // Billing Agreement Id in case of Amazon Payments
|
||||||
dateCreated: Date,
|
dateCreated: Date,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export let schema = new Schema({
|
|||||||
},
|
},
|
||||||
name: {type: String, required: true},
|
name: {type: String, required: true},
|
||||||
challenge: {type: String},
|
challenge: {type: String},
|
||||||
|
group: {type: String},
|
||||||
}, {
|
}, {
|
||||||
strict: true,
|
strict: true,
|
||||||
minimize: false, // So empty objects are returned
|
minimize: false, // So empty objects are returned
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ let dailyTodoSchema = () => {
|
|||||||
text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation
|
text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation
|
||||||
_id: false,
|
_id: false,
|
||||||
id: {type: String, default: shared.uuid, required: true, validate: [validator.isUUID, 'Invalid uuid.']},
|
id: {type: String, default: shared.uuid, required: true, validate: [validator.isUUID, 'Invalid uuid.']},
|
||||||
|
linkId: {type: String},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const NOTIFICATION_TYPES = [
|
|||||||
'GROUP_TASK_APPROVED',
|
'GROUP_TASK_APPROVED',
|
||||||
'LOGIN_INCENTIVE',
|
'LOGIN_INCENTIVE',
|
||||||
'GROUP_INVITE_ACCEPTED',
|
'GROUP_INVITE_ACCEPTED',
|
||||||
|
'SCORED_TASK',
|
||||||
];
|
];
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|||||||
@@ -34,8 +34,9 @@
|
|||||||
button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})')
|
button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})')
|
||||||
span.glyphicon.glyphicon-trash
|
span.glyphicon.glyphicon-trash
|
||||||
ul(ng-if='!_editing', hrpg-sort-tags)
|
ul(ng-if='!_editing', hrpg-sort-tags)
|
||||||
li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: tag.challenge}', ng-repeat='tag in user.tags', bindonce='user.tags')
|
li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: showChallengeClass(tag)}', ng-repeat='tag in user.tags', bindonce='user.tags')
|
||||||
a(ng-click='toggleFilter(tag)')
|
a(ng-click='toggleFilter(tag)')
|
||||||
span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge")
|
span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge")
|
||||||
markdown(text='tag.name')
|
markdown(text='tag.name')
|
||||||
|
| {{tag}}
|
||||||
// <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}">
|
// <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}">
|
||||||
|
|||||||
@@ -366,65 +366,5 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
|
|||||||
span=env.t('unsubscribeAllEmails')
|
span=env.t('unsubscribeAllEmails')
|
||||||
|
|
||||||
small=env.t('unsubscribeAllEmailsText')
|
small=env.t('unsubscribeAllEmailsText')
|
||||||
|
|
||||||
script(id='partials/options.settings.subscription.html',type='text/ng-template')
|
|
||||||
//-h2=env.t('individualSub')
|
|
||||||
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
|
|
||||||
h3= env.t('benefits')
|
|
||||||
.row
|
|
||||||
.col-md-6
|
|
||||||
+subPerks()
|
|
||||||
|
|
||||||
.container-fluid.slight-vertical-padding(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
include ./settings/subscription
|
||||||
h4=env.t('subscribeUsing')
|
|
||||||
.row.text-center
|
|
||||||
.col-xs-12.col-md-3
|
|
||||||
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
|
|
||||||
.col-xs-12.col-md-3
|
|
||||||
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')
|
|
||||||
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
|
||||||
.col-xs-12.col-md-3
|
|
||||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
|
|
||||||
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
|
||||||
|
|
||||||
.col-md-6
|
|
||||||
table.table.alert.alert-info(ng-if='user.purchased.plan.customerId')
|
|
||||||
tr(ng-if='user.purchased.plan.dateTerminated'): td.alert.alert-warning
|
|
||||||
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
|
|
||||||
i.glyphicon.glyphicon-time
|
|
||||||
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
|
||||||
tr(ng-if='!user.purchased.plan.dateTerminated'): td
|
|
||||||
h4=env.t('subscribed')
|
|
||||||
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
|
|
||||||
tr(ng-if='user.purchased.plan.extraMonths'): td
|
|
||||||
span.glyphicon.glyphicon-credit-card
|
|
||||||
| #{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
|
|
||||||
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
|
|
||||||
span.glyphicon.glyphicon-forward
|
|
||||||
| #{env.t('consecutiveSubscription')}
|
|
||||||
ul.list-unstyled
|
|
||||||
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
|
|
||||||
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
|
|
||||||
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
|
|
||||||
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
|
||||||
h4(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
|
|
||||||
.form-group.reduce-top-margin
|
|
||||||
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"')
|
|
||||||
label
|
|
||||||
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
|
|
||||||
span(ng-show='block.original')
|
|
||||||
span.label.label-success.line-through
|
|
||||||
| ${{:: block.original }}
|
|
||||||
=env.t('subscriptionRateText', {price:'{{::block.price}}', months: '{{::block.months}}'})
|
|
||||||
span(ng-hide='block.original')
|
|
||||||
=env.t('subscriptionRateText', {price: '{{::block.price}}', months: '{{block.months}}'})
|
|
||||||
|
|
||||||
.form-inline
|
|
||||||
.form-group
|
|
||||||
input.form-control(type='text', ng-model='_subscription.coupon', placeholder= env.t('couponPlaceholder'))
|
|
||||||
.form-group
|
|
||||||
button.pull-right.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)')= env.t("apply")
|
|
||||||
|
|
||||||
div(ng-if='user.purchased.plan.customerId')
|
|
||||||
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
|
|
||||||
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
|
|
||||||
|
|||||||
63
website/views/options/settings/subscription.jade
Normal file
63
website/views/options/settings/subscription.jade
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
script(id='partials/options.settings.subscription.html',type='text/ng-template', ng-init="groupPane = 'subscription'")
|
||||||
|
//-h2=env.t('individualSub')
|
||||||
|
//- +groupSubscription
|
||||||
|
|
||||||
|
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
|
||||||
|
h3= env.t('benefits')
|
||||||
|
.row
|
||||||
|
.col-md-6
|
||||||
|
+subPerks()
|
||||||
|
|
||||||
|
.col-md-6
|
||||||
|
table.table.alert.alert-info(ng-if='user.purchased.plan.customerId')
|
||||||
|
tr(ng-if='user.purchased.plan.dateTerminated'): td.alert.alert-warning
|
||||||
|
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
|
||||||
|
i.glyphicon.glyphicon-time
|
||||||
|
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
||||||
|
tr(ng-if='!user.purchased.plan.dateTerminated'): td
|
||||||
|
h4=env.t('subscribed')
|
||||||
|
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
|
||||||
|
tr(ng-if='user.purchased.plan.extraMonths'): td
|
||||||
|
span.glyphicon.glyphicon-credit-card
|
||||||
|
| #{env.t('purchasedPlanExtraMonths', {months: '{{user.purchased.plan.extraMonths | number:2}}'})}
|
||||||
|
tr(ng-if='user.purchased.plan.consecutive.count || user.purchased.plan.consecutive.offset'): td
|
||||||
|
span.glyphicon.glyphicon-forward
|
||||||
|
| #{env.t('consecutiveSubscription')}
|
||||||
|
ul.list-unstyled
|
||||||
|
li #{env.t('consecutiveMonths')} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
|
||||||
|
li #{env.t('gemCapExtra')} {{user.purchased.plan.consecutive.gemCapExtra}}
|
||||||
|
li #{env.t('mysticHourglasses')} {{user.purchased.plan.consecutive.trinkets}}
|
||||||
|
div(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
||||||
|
h4(ng-if='(user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')= env.t("resubscribe")
|
||||||
|
.form-group.reduce-top-margin
|
||||||
|
.radio(ng-repeat='block in Content.subscriptionBlocks | toArray | omit: "discount==true" | orderBy:"months"', ng-if="block.type !== 'group'")
|
||||||
|
label
|
||||||
|
input(type="radio", name="subRadio", ng-value="block.key", ng-model='_subscription.key')
|
||||||
|
span(ng-show='block.original')
|
||||||
|
span.label.label-success.line-through
|
||||||
|
| ${{:: block.original }}
|
||||||
|
=env.t('subscriptionRateText', {price:'{{::block.price}}', months: '{{::block.months}}'})
|
||||||
|
span(ng-hide='block.original')
|
||||||
|
=env.t('subscriptionRateText', {price: '{{::block.price}}', months: '{{block.months}}'})
|
||||||
|
|
||||||
|
.form-inline
|
||||||
|
.form-group
|
||||||
|
input.form-control(type='text', ng-model='_subscription.coupon', placeholder= env.t('couponPlaceholder'))
|
||||||
|
.form-group
|
||||||
|
button.pull-right.btn.btn-small(type='button',ng-click='applyCoupon(_subscription.coupon)')= env.t("apply")
|
||||||
|
|
||||||
|
div(ng-if='user.purchased.plan.customerId')
|
||||||
|
.btn.btn-primary(ng-if='!user.purchased.plan.dateTerminated && user.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit()')=env.t('subUpdateCard')
|
||||||
|
.btn.btn-sm.btn-danger(ng-if='!user.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription()')=env.t('cancelSub')
|
||||||
|
|
||||||
|
.container-fluid.slight-vertical-padding(ng-if='!user.purchased.plan.customerId || (user.purchased.plan.customerId && user.purchased.plan.dateTerminated)')
|
||||||
|
small.muted=env.t('subscribeUsing')
|
||||||
|
.row.text-center
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon})', ng-disabled='!_subscription.key')= env.t('card')
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}', ng-disabled='!_subscription.key')
|
||||||
|
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
||||||
|
.col-xs-4
|
||||||
|
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon})")
|
||||||
|
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
|
|
||||||
.form-group
|
|
||||||
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
|
|
||||||
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
|
|
||||||
.form-group
|
|
||||||
label(for='new-group-description')=env.t('description')
|
|
||||||
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')
|
|
||||||
.form-group(ng-show='type=="guild"')
|
|
||||||
.radio
|
|
||||||
label
|
|
||||||
input(type='radio', name='new-group-privacy', value='public', ng-model='newGroup.privacy')
|
|
||||||
=env.t('public')
|
|
||||||
.radio
|
|
||||||
label
|
|
||||||
input(type='radio', name='new-group-privacy', value='private', ng-model='newGroup.privacy')
|
|
||||||
=env.t('inviteOnly')
|
|
||||||
br
|
|
||||||
input.btn.btn-default(type='submit', ng-disabled='!newGroup.privacy && !newGroup.name', value=env.t('create'))
|
|
||||||
span.gem-cost= '4 ' + env.t('gems')
|
|
||||||
p
|
|
||||||
small=env.t('gemCost')
|
|
||||||
.form-group
|
|
||||||
.checkbox
|
|
||||||
label
|
|
||||||
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
|
|
||||||
=env.t('leaderOnlyChallenges')
|
|
||||||
.form-group(ng-show='type=="party"')
|
|
||||||
input.btn.btn-default.form-control(type='submit', value=env.t('create'))
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
include ./groups/group-subscription
|
||||||
|
include ./groups/group-plans-benefits
|
||||||
|
|
||||||
a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter', popover-title=env.t('guildBankPop1'), popover=env.t('guildBankPop2'), popover-placement='left')
|
a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter', popover-title=env.t('guildBankPop1'), popover=env.t('guildBankPop2'), popover-placement='left')
|
||||||
// <span class="task-action-btn tile flush bright add-gems-btn">+</span>
|
// <span class="task-action-btn tile flush bright add-gems-btn">+</span>
|
||||||
span.task-action-btn.tile.flush.neutral
|
span.task-action-btn.tile.flush.neutral
|
||||||
@@ -6,208 +9,174 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
|||||||
=' ' + env.t('guildGems')
|
=' ' + env.t('guildGems')
|
||||||
|
|
||||||
.container-fluid
|
.container-fluid
|
||||||
.row
|
ul.options-menu(ng-show='group.privacy === "private"')
|
||||||
.col-md-4
|
li
|
||||||
|
a(ng-click="groupPanel = 'chat'")=env.t('groupHomeTitle')
|
||||||
// ------ Bosses -------
|
li(ng-show='group.purchased.active')
|
||||||
+boss(false, false)
|
a(ng-click="groupPanel = 'tasks'")=env.t('groupTasksTitle')
|
||||||
|
li(ng-show='group.purchased.active && group.leader._id === user._id')
|
||||||
// ------ Information -------
|
a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
|
||||||
.panel.panel-default
|
li
|
||||||
.panel-heading(bindonce='group')
|
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('subscription')
|
||||||
h3.panel-title
|
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && !group.purchased.plan.customerId')=env.t('upgradeTitle')
|
||||||
span {{group.name}}
|
|
||||||
span.group-leave-join(ng-if='group')
|
|
||||||
a.btn.btn-sm.btn-danger.pull-right(ng-if="isMemberOfGroup(User.user._id, group)", ng-hide='group._editing', ng-click='clickLeave(group, $event)')
|
|
||||||
span.glyphicon.glyphicon-ban-circle
|
|
||||||
=env.t('leave')
|
|
||||||
a.btn.btn-success.pull-right(ng-if='!isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
|
||||||
span(ng-if='group.leader._id == user.id')
|
|
||||||
button.btn.btn-sm.btn-primary.pull-right(ng-click='cancelEdit(group)', ng-hide='!group._editing')=env.t('cancel')
|
|
||||||
button.btn.btn-sm.btn-primary.pull-right(ng-click='saveEdit(group)', ng-show='group._editing')=env.t('save')
|
|
||||||
button.btn.btn-sm.btn-default.pull-right(ng-click='editGroup(group)', ng-hide='group._editing')=env.t('editGroup')
|
|
||||||
|
|
||||||
.panel-body
|
|
||||||
form(ng-show='group._editing')
|
|
||||||
.form-group
|
|
||||||
label=env.t('groupName')
|
|
||||||
input.form-control(type='text', ng-model='groupCopy.name', placeholder=env.t('groupName'))
|
|
||||||
.form-group
|
|
||||||
label=env.t('description')
|
|
||||||
textarea.form-control(rows=6, ng-model='groupCopy.description')
|
|
||||||
include ../../shared/formatting-help
|
|
||||||
.form-group
|
|
||||||
label=env.t('logoUrl')
|
|
||||||
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='groupCopy.logo')
|
|
||||||
.form-group
|
|
||||||
.checkbox
|
|
||||||
label
|
|
||||||
input(type='checkbox', ng-model='groupCopy.leaderOnly.challenges')
|
|
||||||
=env.t('leaderOnlyChallenges')
|
|
||||||
|
|
||||||
h4=env.t('assignLeader')
|
|
||||||
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
|
|
||||||
|
|
||||||
div(ng-show='!group._editing')
|
|
||||||
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
|
|
||||||
markdown(text='group.description')
|
|
||||||
br
|
|
||||||
p=env.t('groupLeader')
|
|
||||||
|:
|
|
||||||
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
|
|
||||||
| {{group.leader.profile.name}}
|
|
||||||
|
|
||||||
.text-center(ng-if='group.type === "party"')
|
|
||||||
.row.row-margin: .col-sm-6.col-sm-offset-3
|
|
||||||
button.btn.btn-success.btn-block(
|
|
||||||
ng-if='!group.quest.key',
|
|
||||||
ng-click='clickStartQuest();'
|
|
||||||
)=env.t('startAQuest')
|
|
||||||
|
|
||||||
// ------ Members -------
|
|
||||||
.panel.panel-default
|
|
||||||
.panel-heading
|
|
||||||
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="openInviteModal(group)")=env.t("inviteFriends")
|
|
||||||
|
|
||||||
.panel-body.modal-fixed-height
|
|
||||||
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
|
|
||||||
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
|
|
||||||
tr(ng-repeat='member in group.members track by member._id')
|
|
||||||
td.media
|
|
||||||
// allow leaders to ban members
|
|
||||||
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
|
|
||||||
a.media-object(ng-click='removeMember(group, member, true)')
|
|
||||||
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
|
|
||||||
a.media-body
|
|
||||||
span(ng-click='clickMember(member._id, true)')
|
|
||||||
| {{member.profile.name}}
|
|
||||||
span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
|
|
||||||
| (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
|
|
||||||
.pull-right(ng-if='group.type === "party"')
|
|
||||||
span.text-success {{member.online ? '● ' + env.t('online') : ''}}
|
|
||||||
tr(ng-if='::group.memberCount > group.members.length')
|
|
||||||
td
|
|
||||||
span.badge {{group.memberCount - group.members.length}}
|
|
||||||
= ' ' + env.t('moreMembers')
|
|
||||||
h4(ng-show='group.invites.length > 0')=env.t('invited')
|
|
||||||
table.table.table-striped
|
|
||||||
tr(ng-repeat='invite in group.invites')
|
|
||||||
td.media
|
|
||||||
// allow leaders to ban members
|
|
||||||
.pull-left(ng-show='group.leader._id == user.id')
|
|
||||||
a.media-object(ng-click='removeMember(group, invite, false)')
|
|
||||||
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
|
|
||||||
a.media-body
|
|
||||||
span(ng-click='clickMember(invite._id, true)')
|
|
||||||
| {{invite.profile.name}}
|
|
||||||
|
|
||||||
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
|
|
||||||
.panel-heading
|
|
||||||
h3.panel-title=env.t('partyList')
|
|
||||||
.panel-body
|
|
||||||
.form-group
|
|
||||||
select.form-control#partyOrder(
|
|
||||||
ng-model='user.party.order',
|
|
||||||
ng-controller='ChatCtrl',
|
|
||||||
ng-options='k as v for (k , v) in partyOrderChoices',
|
|
||||||
ng-change='set({"party.order": user.party.order})'
|
|
||||||
)
|
|
||||||
|
|
|
||||||
select.form-control#partyOrderAscending(
|
|
||||||
ng-model='user.party.orderAscending',
|
|
||||||
ng-controller='ChatCtrl',
|
|
||||||
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
|
|
||||||
ng-change='set({"party.orderAscending": user.party.orderAscending})'
|
|
||||||
)
|
|
||||||
|
|
||||||
include ./challenge-box
|
|
||||||
|
|
||||||
div(ng-if="group")
|
|
||||||
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
|
||||||
|
|
||||||
.slight-vertical-padding
|
|
||||||
small.muted=env.t('groupID')
|
|
||||||
| : {{group._id}}
|
|
||||||
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
|
|
||||||
small.muted=env.t('userId')
|
|
||||||
| : {{user._id}}
|
|
||||||
|
|
||||||
.col-md-8
|
|
||||||
div
|
|
||||||
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
|
|
||||||
.slight-vertical-padding
|
|
||||||
table(ng-show='group.leaderMessage')
|
|
||||||
tr
|
|
||||||
td
|
|
||||||
.popover.static-popover.fade.right.in.wide-popover
|
|
||||||
.arrow
|
|
||||||
h3.popover-title {{group.leader.profile.name}}
|
|
||||||
.popover-content
|
|
||||||
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
|
|
||||||
|
|
||||||
ul.options-menu(ng-init="groupPane = 'chat'", ng-hide="true")
|
|
||||||
//- li
|
|
||||||
//- a(ng-click="groupPane = 'chat'")=env.t('chat')
|
|
||||||
//- li
|
|
||||||
//- a(ng-click="groupPane = 'tasks'", ng-show='group.purchased.active')=env.t('tasks')
|
|
||||||
//- li
|
|
||||||
//- a(ng-click="groupPane = 'approvals'", ng-show='group.purchased.active && group.leader._id === user._id')=env.t('approvals')
|
|
||||||
//- li
|
|
||||||
//- a(ng-click="groupPane = 'subscription'", ng-show='group.leader._id === user._id')=env.t('subscription')
|
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
.tab-pane.active
|
.tab-pane.active
|
||||||
|
|
||||||
div(ng-controller='ChatCtrl', ng-show="groupPane == 'chat'")
|
.row(ng-show="groupPanel == 'chat'")
|
||||||
.alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote')
|
.col-md-4
|
||||||
h3=env.t('chat')
|
|
||||||
include ./chat-box
|
|
||||||
|
|
||||||
+chatMessages()
|
// ------ Bosses -------
|
||||||
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
|
+boss(false, false)
|
||||||
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
|
|
||||||
|
|
||||||
group-tasks(ng-show="groupPane == 'tasks'")
|
|
||||||
|
|
||||||
group-approvals(ng-show="groupPane == 'approvals'", ng-if="group.leader._id === user._id", group="group")
|
|
||||||
|
|
||||||
//TODO: This can be a directive and the group/user can be an object passed via attribute
|
|
||||||
div(ng-show="groupPane == 'subscription'")
|
|
||||||
.col-md-12
|
|
||||||
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
|
|
||||||
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
|
|
||||||
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
|
|
||||||
i.glyphicon.glyphicon-time
|
|
||||||
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
|
||||||
tr(ng-if='!group.purchased.plan.dateTerminated'): td
|
|
||||||
h4=env.t('subscribed')
|
|
||||||
p(ng-if='group.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[group.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[group.purchased.plan.planId].months}}', plan: '{{group.purchased.plan.paymentMethod}}'})
|
|
||||||
tr(ng-if='group.purchased.plan.extraMonths'): td
|
|
||||||
span.glyphicon.glyphicon-credit-card
|
|
||||||
| #{env.t('purchasedPlanExtraMonths', {months: '{{group.purchased.plan.extraMonths | number:2}}'})}
|
|
||||||
tr(ng-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset'): td
|
|
||||||
span.glyphicon.glyphicon-forward
|
|
||||||
| #{env.t('consecutiveSubscription')}
|
|
||||||
ul.list-unstyled
|
|
||||||
li #{env.t('consecutiveMonths')} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
|
|
||||||
li #{env.t('gemCapExtra')} {{group.purchased.plan.consecutive.gemCapExtra}}
|
|
||||||
li #{env.t('mysticHourglasses')} {{group.purchased.plan.consecutive.trinkets}}
|
|
||||||
|
|
||||||
div(ng-if='group.purchased.plan.customerId')
|
// ------ Information -------
|
||||||
.btn.btn-primary(ng-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit({groupId: group.id})')=env.t('subUpdateCard')
|
.panel.panel-default
|
||||||
.btn.btn-sm.btn-danger(ng-if='!group.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription({group: group})')=env.t('cancelSub')
|
.panel-heading(bindonce='group')
|
||||||
|
h3.panel-title
|
||||||
|
span {{group.name}}
|
||||||
|
span.group-leave-join(ng-if='group')
|
||||||
|
a.btn.btn-sm.btn-danger.pull-right(ng-if="isMemberOfGroup(User.user._id, group)", ng-hide='group._editing', ng-click='clickLeave(group, $event)')
|
||||||
|
span.glyphicon.glyphicon-ban-circle
|
||||||
|
=env.t('leave')
|
||||||
|
a.btn.btn-success.pull-right(ng-if='!isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
||||||
|
span(ng-if='group.leader._id == user.id')
|
||||||
|
button.btn.btn-sm.btn-primary.pull-right(ng-click='cancelEdit(group)', ng-hide='!group._editing')=env.t('cancel')
|
||||||
|
button.btn.btn-sm.btn-primary.pull-right(ng-click='saveEdit(group)', ng-show='group._editing')=env.t('save')
|
||||||
|
button.btn.btn-sm.btn-default.pull-right(ng-click='editGroup(group)', ng-hide='group._editing')=env.t('editGroup')
|
||||||
|
|
||||||
.container-fluid.slight-vertical-padding(ng-if='!group.purchased.plan.customerId || (group.purchased.plan.customerId && group.purchased.plan.dateTerminated)', ng-init="_subscription.key='group_monthly'")
|
.panel-body
|
||||||
small.muted=env.t('subscribeUsing')
|
form(ng-show='group._editing')
|
||||||
.row.text-center
|
.form-group
|
||||||
.col-xs-4
|
label=env.t('groupName')
|
||||||
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})', ng-disabled='!_subscription.key')= env.t('card')
|
input.form-control(type='text', ng-model='groupCopy.name', placeholder=env.t('groupName'))
|
||||||
.col-xs-4
|
.form-group
|
||||||
a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
|
label=env.t('description')
|
||||||
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
textarea.form-control(rows=6, ng-model='groupCopy.description')
|
||||||
.col-xs-4
|
include ../../shared/formatting-help
|
||||||
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})")
|
.form-group
|
||||||
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
label=env.t('logoUrl')
|
||||||
|
input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='groupCopy.logo')
|
||||||
|
.form-group
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input(type='checkbox', ng-model='groupCopy.leaderOnly.challenges')
|
||||||
|
=env.t('leaderOnlyChallenges')
|
||||||
|
|
||||||
|
h4=env.t('assignLeader')
|
||||||
|
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
|
||||||
|
|
||||||
|
div(ng-show='!group._editing')
|
||||||
|
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
|
||||||
|
markdown(text='group.description')
|
||||||
|
br
|
||||||
|
p=env.t('groupLeader')
|
||||||
|
|:
|
||||||
|
a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
|
||||||
|
| {{group.leader.profile.name}}
|
||||||
|
|
||||||
|
.text-center(ng-if='group.type === "party"')
|
||||||
|
.row.row-margin: .col-sm-6.col-sm-offset-3
|
||||||
|
button.btn.btn-success.btn-block(
|
||||||
|
ng-if='!group.quest.key',
|
||||||
|
ng-click='clickStartQuest();'
|
||||||
|
)=env.t('startAQuest')
|
||||||
|
|
||||||
|
// ------ Members -------
|
||||||
|
.panel.panel-default
|
||||||
|
.panel-heading
|
||||||
|
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="openInviteModal(group)")=env.t("inviteFriends")
|
||||||
|
|
||||||
|
.panel-body.modal-fixed-height
|
||||||
|
h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
|
||||||
|
table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
|
||||||
|
tr(ng-repeat='member in group.members track by member._id')
|
||||||
|
td.media
|
||||||
|
// allow leaders to ban members
|
||||||
|
.pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
|
||||||
|
a.media-object(ng-click='removeMember(group, member, true)')
|
||||||
|
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
|
||||||
|
a.media-body
|
||||||
|
span(ng-click='clickMember(member._id, true)')
|
||||||
|
| {{member.profile.name}}
|
||||||
|
span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
|
||||||
|
| (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
|
||||||
|
.pull-right(ng-if='group.type === "party"')
|
||||||
|
span.text-success {{member.online ? '● ' + env.t('online') : ''}}
|
||||||
|
tr(ng-if='::group.memberCount > group.members.length')
|
||||||
|
td
|
||||||
|
span.badge {{group.memberCount - group.members.length}}
|
||||||
|
= ' ' + env.t('moreMembers')
|
||||||
|
h4(ng-show='group.invites.length > 0')=env.t('invited')
|
||||||
|
table.table.table-striped
|
||||||
|
tr(ng-repeat='invite in group.invites')
|
||||||
|
td.media
|
||||||
|
// allow leaders to ban members
|
||||||
|
.pull-left(ng-show='group.leader._id == user.id')
|
||||||
|
a.media-object(ng-click='removeMember(group, invite, false)')
|
||||||
|
span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
|
||||||
|
a.media-body
|
||||||
|
span(ng-click='clickMember(invite._id, true)')
|
||||||
|
| {{invite.profile.name}}
|
||||||
|
|
||||||
|
.panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
|
||||||
|
.panel-heading
|
||||||
|
h3.panel-title=env.t('partyList')
|
||||||
|
.panel-body
|
||||||
|
.form-group
|
||||||
|
select.form-control#partyOrder(
|
||||||
|
ng-model='user.party.order',
|
||||||
|
ng-controller='ChatCtrl',
|
||||||
|
ng-options='k as v for (k , v) in partyOrderChoices',
|
||||||
|
ng-change='set({"party.order": user.party.order})'
|
||||||
|
)
|
||||||
|
|
|
||||||
|
select.form-control#partyOrderAscending(
|
||||||
|
ng-model='user.party.orderAscending',
|
||||||
|
ng-controller='ChatCtrl',
|
||||||
|
ng-options='k as v for (k , v) in partyOrderAscendingChoices',
|
||||||
|
ng-change='set({"party.orderAscending": user.party.orderAscending})'
|
||||||
|
)
|
||||||
|
|
||||||
|
include ./challenge-box
|
||||||
|
|
||||||
|
div(ng-if="group")
|
||||||
|
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
||||||
|
|
||||||
|
.slight-vertical-padding
|
||||||
|
small.muted=env.t('groupID')
|
||||||
|
| : {{group._id}}
|
||||||
|
.slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
|
||||||
|
small.muted=env.t('userId')
|
||||||
|
| : {{user._id}}
|
||||||
|
|
||||||
|
.col-md-8
|
||||||
|
div
|
||||||
|
textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
|
||||||
|
.slight-vertical-padding
|
||||||
|
table(ng-show='group.leaderMessage')
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
.popover.static-popover.fade.right.in.wide-popover
|
||||||
|
.arrow
|
||||||
|
h3.popover-title {{group.leader.profile.name}}
|
||||||
|
.popover-content
|
||||||
|
markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
|
||||||
|
|
||||||
|
div(ng-controller='ChatCtrl')
|
||||||
|
.alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote')
|
||||||
|
h3=env.t('chat')
|
||||||
|
include ./chat-box
|
||||||
|
|
||||||
|
+chatMessages()
|
||||||
|
h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
|
||||||
|
h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
|
||||||
|
|
||||||
|
group-tasks(ng-show="groupPanel == 'tasks'")
|
||||||
|
|
||||||
|
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
|
||||||
|
|
||||||
|
+groupSubscription
|
||||||
|
|
||||||
|
|||||||
29
website/views/options/social/groups/create-group.jade
Normal file
29
website/views/options/social/groups/create-group.jade
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
mixin groupCreateForm()
|
||||||
|
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
|
||||||
|
.form-group
|
||||||
|
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
|
||||||
|
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
|
||||||
|
.form-group
|
||||||
|
label(for='new-group-description')=env.t('description')
|
||||||
|
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')
|
||||||
|
.form-group(ng-show='type=="guild"')
|
||||||
|
.radio
|
||||||
|
label
|
||||||
|
input(type='radio', name='new-group-privacy', value='public', ng-model='newGroup.privacy')
|
||||||
|
=env.t('public')
|
||||||
|
.radio
|
||||||
|
label
|
||||||
|
input(type='radio', name='new-group-privacy', value='private', ng-model='newGroup.privacy')
|
||||||
|
=env.t('inviteOnly')
|
||||||
|
br
|
||||||
|
input.btn.btn-default(type='submit', ng-disabled='!newGroup.privacy && !newGroup.name', value=env.t('create'))
|
||||||
|
span.gem-cost= '4 ' + env.t('gems')
|
||||||
|
p
|
||||||
|
small=env.t('gemCost')
|
||||||
|
.form-group
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
|
||||||
|
=env.t('leaderOnlyChallenges')
|
||||||
|
.form-group(ng-show='type=="party"')
|
||||||
|
input.btn.btn-default.form-control(type='submit', value=env.t('create'))
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
mixin groupPlansBenefits()
|
||||||
|
h2.text-center=env.t('groupBenefitsTitle')
|
||||||
|
.row(style="font-size: 2rem;")
|
||||||
|
.col-md-6.col-md-offset-3=env.t('groupBenefitsDescription')
|
||||||
|
.row
|
||||||
|
.col-md-5.col-md-offset-4
|
||||||
|
div
|
||||||
|
h3
|
||||||
|
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
|
||||||
|
=env.t('groupBenefitOneTitle')
|
||||||
|
span=env.t('groupBenefitOneDescription')
|
||||||
|
div
|
||||||
|
h3
|
||||||
|
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
|
||||||
|
=env.t('groupBenefitTwoTitle')
|
||||||
|
span=env.t('groupBenefitTwoDescription')
|
||||||
|
div
|
||||||
|
h3
|
||||||
|
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
|
||||||
|
=env.t('groupBenefitThreeTitle')
|
||||||
|
span=env.t('groupBenefitThreeDescription')
|
||||||
|
div
|
||||||
|
h3
|
||||||
|
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
|
||||||
|
=env.t('groupBenefitFourTitle')
|
||||||
|
span=env.t('groupBenefitFourDescription')
|
||||||
|
div
|
||||||
|
h3
|
||||||
|
span.glyphicon.glyphicon-ok-circle(style='margin-right: 1.5rem;')
|
||||||
|
=env.t('groupBenefitFiveTitle')
|
||||||
|
span=env.t('groupBenefitFiveDescription')
|
||||||
40
website/views/options/social/groups/group-plans.jade
Normal file
40
website/views/options/social/groups/group-plans.jade
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
include ./create-group
|
||||||
|
|
||||||
|
script(type='text/ng-template', id='partials/options.social.groupPlans.html')
|
||||||
|
div(ng-show='activePage === PAGES.BENEFITS')
|
||||||
|
+groupPlansBenefits
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
.row
|
||||||
|
.col-sm-6.col-sm-offset-3
|
||||||
|
a.btn.btn-primary.btn-lg.btn-block(ng-click="changePage(PAGES.CREATE_GROUP)")=env.t('createAGroup')
|
||||||
|
|
||||||
|
div(ng-show='activePage === PAGES.CREATE_GROUP')
|
||||||
|
h2.text-center=env.t('createAGroup')
|
||||||
|
|
||||||
|
.col-xs-12
|
||||||
|
+groupCreateForm
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
.row
|
||||||
|
.col-sm-6.col-sm-offset-3
|
||||||
|
a.btn.btn-primary.btn-lg.btn-block(ng-click="createGroup()", ng-disabled="!newGroupIsReady()")=env.t('create')
|
||||||
|
|
||||||
|
div(ng-show='activePage === PAGES.UPGRADE_GROUP')
|
||||||
|
h2.text-center=env.t('upgradeTitle')
|
||||||
|
|
||||||
|
.row.text-center
|
||||||
|
.col-md-6.col-md-offset-3
|
||||||
|
a.purchase.btn.btn-primary(ng-click='upgradeGroup(PAYMENTS.STRIPE)')=env.t('card')
|
||||||
|
a.purchase(ng-click='upgradeGroup(PAYMENTS.AMAZON)')
|
||||||
|
img(src='https://payments.amazon.com/gp/cba/button', alt=env.t('amazonPayments'))
|
||||||
|
//- .col-xs-4
|
||||||
|
//- a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
|
||||||
|
//- img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-md-6.col-md-offset-3
|
||||||
|
br
|
||||||
|
.text-center=env.t('groupSubscriptionPrice')
|
||||||
50
website/views/options/social/groups/group-subscription.jade
Normal file
50
website/views/options/social/groups/group-subscription.jade
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// @TODO: This can be a directive and the group/user can be an object passed via attribute
|
||||||
|
mixin groupSubscription()
|
||||||
|
div(ng-show="groupPanel == 'subscription'")
|
||||||
|
.col-md-12
|
||||||
|
.col-md-12
|
||||||
|
div(ng-hide="group.purchased.plan.customerId")
|
||||||
|
+groupPlansBenefits
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-md-12
|
||||||
|
br
|
||||||
|
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
|
||||||
|
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
|
||||||
|
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
|
||||||
|
i.glyphicon.glyphicon-time
|
||||||
|
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
|
||||||
|
tr(ng-if='!group.purchased.plan.dateTerminated'): td
|
||||||
|
h3=env.t('subscribed')
|
||||||
|
p(ng-if='group.purchased.plan.planId')=env.t('groupSubscriptionPrice')
|
||||||
|
tr(ng-if='group.purchased.plan.extraMonths'): td
|
||||||
|
span.glyphicon.glyphicon-credit-card
|
||||||
|
| #{env.t('purchasedPlanExtraMonths', {months: '{{group.purchased.plan.extraMonths | number:2}}'})}
|
||||||
|
tr(ng-if='group.purchased.plan.consecutive.count || group.purchased.plan.consecutive.offset'): td
|
||||||
|
span.glyphicon.glyphicon-forward
|
||||||
|
| #{env.t('consecutiveSubscription')}
|
||||||
|
ul.list-unstyled
|
||||||
|
li #{env.t('consecutiveMonths')} {{group.purchased.plan.consecutive.count + group.purchased.plan.consecutive.offset}}
|
||||||
|
li #{env.t('gemCapExtra')} {{group.purchased.plan.consecutive.gemCapExtra}}
|
||||||
|
li #{env.t('mysticHourglasses')} {{group.purchased.plan.consecutive.trinkets}}
|
||||||
|
|
||||||
|
div(ng-if='group.purchased.plan.customerId')
|
||||||
|
.btn.btn-primary(ng-if='!group.purchased.plan.dateTerminated && group.purchased.plan.paymentMethod=="Stripe"', ng-click='Payments.showStripeEdit({groupId: group.id})')=env.t('subUpdateCard')
|
||||||
|
.btn.btn-sm.btn-danger(ng-if='!group.purchased.plan.dateTerminated', ng-click='Payments.cancelSubscription({group: group})')=env.t('cancelSub')
|
||||||
|
|
||||||
|
.row
|
||||||
|
.col-md-12(ng-if='!group.purchased.plan.customerId || (group.purchased.plan.customerId && group.purchased.plan.dateTerminated)', ng-init="_subscription.key='group_monthly'")
|
||||||
|
.row.text-center
|
||||||
|
h3 Upgrade My Group
|
||||||
|
div
|
||||||
|
a.purchase.btn.btn-primary(ng-click='Payments.showStripe({subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})', ng-disabled='!_subscription.key')= env.t('card')
|
||||||
|
a.purchase(ng-click="Payments.amazonPayments.init({type: 'subscription', subscription:_subscription.key, coupon:_subscription.coupon, groupId: group.id})")
|
||||||
|
img(src='https://payments.amazon.com/gp/cba/button',alt=env.t('amazonPayments'))
|
||||||
|
//- .col-xs-4
|
||||||
|
//- a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
|
||||||
|
//- img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
|
||||||
|
|
||||||
|
.row(ng-if='!group.purchased.plan.customerId')
|
||||||
|
.col-md-6.col-md-offset-3
|
||||||
|
br
|
||||||
|
.text-center=env.t('groupSubscriptionPrice')
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
script(type='text/ng-template', id='partials/groups.tasks.approvals.html')
|
script(type='text/ng-template', id='partials/groups.tasks.approvals.html')
|
||||||
.panel-group(ng-repeat="approval in approvals")
|
.row(style="margin-bottom: 2rem;")
|
||||||
|
.col-md-12
|
||||||
|
button.btn.btn-primary(ng-click='refreshApprovals()', ng-hide="loading")=env.t('refreshApprovals')
|
||||||
|
button.btn.btn-primary(ng-disabled="true", ng-show="loading")=env.t('loading')
|
||||||
|
|
||||||
|
.well(ng-show="group.approvals.length === 0")=env.t('blankApprovalsDescription')
|
||||||
|
|
||||||
|
.panel-group(ng-repeat="approval in group.approvals")
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
span {{approvalTitle(approval)}}
|
span {{approvalTitle(approval)}}
|
||||||
a.btn.btn-sm.btn-success.pull-right(ng-click="approve(approval.group.taskId, approval.userId._id)")=env.t('approve')
|
a.btn.btn-sm.btn-success.pull-right(ng-click="approve(approval.group.taskId, approval.userId._id, approval.userId.profile.name, $index)")=env.t('approve')
|
||||||
|
|||||||
@@ -4,4 +4,10 @@ include ./group-members-autocomplete
|
|||||||
include ./group-tasks-approvals
|
include ./group-tasks-approvals
|
||||||
|
|
||||||
script(type='text/ng-template', id='partials/groups.tasks.html')
|
script(type='text/ng-template', id='partials/groups.tasks.html')
|
||||||
habitrpg-tasks(main=false)
|
.row(style="margin-bottom: 2rem;")
|
||||||
|
.col-md-12
|
||||||
|
button.btn.btn-primary(ng-click='refreshTasks()', ng-hide="loading")=env.t('refreshGroupTasks')
|
||||||
|
button.btn.btn-primary(ng-disabled="true", ng-show="loading")=env.t('loading')
|
||||||
|
|
||||||
|
.row
|
||||||
|
habitrpg-tasks(main=false)
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ include ./quests/index
|
|||||||
include ./chat-message
|
include ./chat-message
|
||||||
include ./party
|
include ./party
|
||||||
include ./groups/group-tasks
|
include ./groups/group-tasks
|
||||||
|
include ./groups/group-plans
|
||||||
|
include ./groups/create-group
|
||||||
|
|
||||||
script(type='text/ng-template', id='partials/options.social.inbox.html')
|
script(type='text/ng-template', id='partials/options.social.inbox.html')
|
||||||
.options-blurbmenu
|
.options-blurbmenu
|
||||||
@@ -65,7 +67,7 @@ script(type='text/ng-template', id='partials/options.social.guilds.detail.html')
|
|||||||
|
|
||||||
script(type='text/ng-template', id='partials/options.social.guilds.create.html')
|
script(type='text/ng-template', id='partials/options.social.guilds.create.html')
|
||||||
div.col-xs-12
|
div.col-xs-12
|
||||||
include ./create-group
|
+groupCreateForm
|
||||||
|
|
||||||
script(type='text/ng-template', id='partials/options.social.guilds.html')
|
script(type='text/ng-template', id='partials/options.social.guilds.html')
|
||||||
ul.options-submenu
|
ul.options-submenu
|
||||||
@@ -105,6 +107,9 @@ script(type='text/ng-template', id='partials/options.social.html')
|
|||||||
li(ng-class="{ active: $state.includes('options.social.hall') }")
|
li(ng-class="{ active: $state.includes('options.social.hall') }")
|
||||||
a(ui-sref='options.social.hall.heroes')
|
a(ui-sref='options.social.hall.heroes')
|
||||||
=env.t('hall')
|
=env.t('hall')
|
||||||
|
li(ng-class="{ active: $state.includes('options.social.groupPlans') }")
|
||||||
|
a(ui-sref='options.social.groupPlans')
|
||||||
|
=env.t('groupPlansTitle')
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
.tab-pane.active
|
.tab-pane.active
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ nav.toolbar(ng-controller='MenuCtrl')
|
|||||||
a(ui-sref='options.social.challenges')=env.t('challenges')
|
a(ui-sref='options.social.challenges')=env.t('challenges')
|
||||||
li
|
li
|
||||||
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
||||||
|
li
|
||||||
|
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
|
||||||
ul.toolbar-submenu
|
ul.toolbar-submenu
|
||||||
li
|
li
|
||||||
a(ui-sref='options.inventory.drops')=env.t('market')
|
a(ui-sref='options.inventory.drops')=env.t('market')
|
||||||
@@ -120,6 +122,8 @@ nav.toolbar(ng-controller='MenuCtrl')
|
|||||||
a(ui-sref='options.social.challenges')=env.t('challenges')
|
a(ui-sref='options.social.challenges')=env.t('challenges')
|
||||||
li
|
li
|
||||||
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
||||||
|
li
|
||||||
|
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
|
||||||
li.toolbar-button-dropdown
|
li.toolbar-button-dropdown
|
||||||
a(ui-sref='options.inventory.drops', data-close-menu)
|
a(ui-sref='options.inventory.drops', data-close-menu)
|
||||||
span=env.t('inventory')
|
span=env.t('inventory')
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ include ./enable-desktop-notifications.jade
|
|||||||
include ./login-incentives.jade
|
include ./login-incentives.jade
|
||||||
include ./login-incentives-reward-unlocked.jade
|
include ./login-incentives-reward-unlocked.jade
|
||||||
include ./generic.jade
|
include ./generic.jade
|
||||||
|
include ./tasks-edit.jade
|
||||||
|
|
||||||
//- Settings
|
//- Settings
|
||||||
script(type='text/ng-template', id='modals/change-day-start.html')
|
script(type='text/ng-template', id='modals/change-day-start.html')
|
||||||
|
|||||||
7
website/views/shared/modals/tasks-edit.jade
Normal file
7
website/views/shared/modals/tasks-edit.jade
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
script(type='text/ng-template', id='modals/task-edit.html')
|
||||||
|
.modal-content.task-modal(style='min-width:28em', class='{{taskStatus}}', id="task-{{task._id}}")
|
||||||
|
.modal-body.text-center(style='padding-bottom:0')
|
||||||
|
| {{scope}}
|
||||||
|
include ../tasks/edit/index
|
||||||
|
.modal-footer(style='margin-top:0')
|
||||||
|
.container-fluid
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
div(ng-if='task._editing')
|
div(ng-if='task._editing')
|
||||||
.task-options
|
.task-options
|
||||||
|
h2 {{task._edit.text}}
|
||||||
|
|
||||||
// Broken Challenge
|
// Broken Challenge
|
||||||
.well(ng-if='task.challenge.broken')
|
.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"')
|
||||||
@@ -33,7 +34,7 @@ div(ng-if='task._editing')
|
|||||||
|
|
||||||
include ./checklist
|
include ./checklist
|
||||||
|
|
||||||
form(ng-submit='saveTask(task,false,true)')
|
form
|
||||||
include ./text_notes
|
include ./text_notes
|
||||||
|
|
||||||
include ./habits/plus_minus
|
include ./habits/plus_minus
|
||||||
@@ -49,4 +50,11 @@ div(ng-if='task._editing')
|
|||||||
include ./advanced_options
|
include ./advanced_options
|
||||||
|
|
||||||
.save-close
|
.save-close
|
||||||
button(type='submit')=env.t('saveAndClose')
|
button(type='submit', ng-click='saveTask(task,false,true); $close()')=env.t('saveAndClose')
|
||||||
|
|
||||||
|
br
|
||||||
|
br
|
||||||
|
|
||||||
|
.save-close
|
||||||
|
button(ng-click='cancelTaskEdit(task); $close()')=env.t('cancel')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
fieldset.option-group(ng-if='!$state.includes("options.social.challenges")')
|
fieldset.option-group(ng-if='!$state.includes("options.social.challenges") && !obj.leader')
|
||||||
p.option-title.mega(ng-class='{active: task._tags}', ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags')
|
p.option-title.mega(ng-class='{active: task._tags}', ng-click='task._tags = !task._tags', tooltip=env.t('expandCollapse'))=env.t('tags')
|
||||||
label.checkbox(ng-repeat='tag in user.tags', ng-if='task._tags')
|
label.checkbox(ng-repeat='tag in user.tags', ng-if='task._tags', style='text-align: left;')
|
||||||
input(type='checkbox', ng-checked="task.tags.indexOf(tag.id) !== -1", ng-click="updateTaskTags(tag.id, task)")
|
input(type='checkbox', ng-checked="task.tags.indexOf(tag.id) !== -1", ng-click="updateTaskTags(tag.id, task)")
|
||||||
markdown(text='tag.name')
|
markdown(text='tag.name')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ include ./task_view/mixins
|
|||||||
script(id='templates/habitrpg-tasks.html', type="text/ng-template")
|
script(id='templates/habitrpg-tasks.html', type="text/ng-template")
|
||||||
.tasks-lists.container-fluid
|
.tasks-lists.container-fluid
|
||||||
.row
|
.row
|
||||||
.col-md-3.col-sm-6(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-md": list.type==="todo", "col-md-3": obj.type }')
|
.col-sm-6(ng-repeat='list in lists', ng-class='::{ "rewards-module": list.type==="reward", "new-row-md": list.type==="todo", "col-md-3": !obj.type }')
|
||||||
.task-column(class='{{::list.type}}s')
|
.task-column(class='{{::list.type}}s')
|
||||||
|
|
||||||
include ./task_view/graph
|
include ./task_view/graph
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
// Due Date
|
// Due Date
|
||||||
span(ng-if='task.type=="todo" && task.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))}}
|
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')
|
||||||
|
span(tooltip=env.t('approvalRequested'))
|
||||||
|
span=env.t('approvalRequested')
|
||||||
|
|
|
||||||
|
|
||||||
// Streak
|
// Streak
|
||||||
|
|
|
|
||||||
@@ -24,18 +31,21 @@
|
|||||||
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'))
|
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}}
|
|{{checklistCompletion(task.checklist)}}/{{task.checklist.length}}
|
||||||
span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)')
|
span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)')
|
||||||
|
|
||||||
// edit
|
// edit
|
||||||
a(ng-hide='task._editing', ng-click='editTask(task, user)', tooltip=env.t('edit'))
|
a(ng-hide='task._editing || (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(ng-hide='task._editing')
|
span.glyphicon.glyphicon-pencil(ng-hide='task._editing')
|
||||||
|
|
|
|
||||||
a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel'))
|
a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel'))
|
||||||
span.glyphicon.glyphicon-remove(ng-hide='!task._editing')
|
span.glyphicon.glyphicon-remove(ng-hide='!task._editing')
|
||||||
|
|
|
|
||||||
|
|
||||||
// save
|
// save
|
||||||
a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save'))
|
a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save'))
|
||||||
span.glyphicon.glyphicon-ok(ng-hide='!task._editing')
|
span.glyphicon.glyphicon-ok(ng-hide='!task._editing')
|
||||||
|
|
|
|
||||||
|
|
||||||
//challenges
|
//challenges
|
||||||
span(ng-if='task.challenge.id')
|
span(ng-if='task.challenge.id')
|
||||||
span(ng-if='task.challenge.broken')
|
span(ng-if='task.challenge.broken')
|
||||||
@@ -44,8 +54,9 @@
|
|||||||
span(ng-if='!task.challenge.broken')
|
span(ng-if='!task.challenge.broken')
|
||||||
span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge'))
|
span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge'))
|
||||||
|
|
|
|
||||||
|
|
||||||
// delete
|
// delete
|
||||||
a(ng-if='!task.challenge.id || obj.leader._id === User.user._id', ng-click='removeTask(task, obj)', tooltip=env.t('delete'))
|
a(ng-if='!task.challenge.id || obj.leader._id === User.user._id', ng-hide="(checkGroupAccess && !checkGroupAccess(obj))" ng-click='removeTask(task, obj)', tooltip=env.t('delete'))
|
||||||
span.glyphicon.glyphicon-trash
|
span.glyphicon.glyphicon-trash
|
||||||
|
|
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ li(id='task-{{::task._id}}',
|
|||||||
ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)',
|
ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)',
|
||||||
ng-show='shouldShow(task, list, user.preferences)',
|
ng-show='shouldShow(task, list, user.preferences)',
|
||||||
popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}',
|
popover-trigger='mouseenter', popover-placement="top", popover-append-to-body='{{::modal ? "false":"true"}}',
|
||||||
data-popover-html="{{task.popoverOpen ? '' : task.notes | markdown}}")
|
data-popover-html="{{::taskPopover(task) | markdown}}")
|
||||||
|
|
||||||
ng-form(name='taskForm')
|
ng-form(name='taskForm')
|
||||||
include ./meta_controls
|
include ./meta_controls
|
||||||
|
|
||||||
include ./task_view/index
|
include ./task_view/index
|
||||||
|
|
||||||
include ./edit/index
|
|
||||||
|
|
||||||
div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]')
|
div(class='{{obj._id}}{{task._id}}-chart', ng-show='charts[obj._id+task._id]')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
form.task-add(name='new{{list.type}}form', ng-hide='obj._locked', ng-submit='addTask(list, obj)', novalidate)
|
form.task-add(name='new{{list.type}}form', ng-hide='obj._locked || (checkGroupAccess && !checkGroupAccess(obj))', ng-submit='addTask(list, obj)', novalidate)
|
||||||
textarea(rows='6', focus-element='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(list, obj)"}', required)
|
textarea(rows='6', focus-element='list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolderBulk}}', ng-if='list.bulk', ui-keydown='{"meta-enter ctrl-enter":"addTask(list, obj)"}', required)
|
||||||
input(type='text', focus-element='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required)
|
input(type='text', focus-element='!list.bulk && list.focus', ng-model='list.newTask', placeholder='{{list.placeHolder}}', ng-if='!list.bulk', required)
|
||||||
button(type='submit', ng-disabled='new{{list.type}}form.$invalid')
|
button(type='submit', ng-disabled='new{{list.type}}form.$invalid')
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
span.reward-cost {{task.value}}
|
span.reward-cost {{task.value}}
|
||||||
|
|
||||||
// Daily & Todos
|
// Daily & Todos
|
||||||
span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"')
|
span.task-checker.action-yesno(ng-if='::task.type=="daily" || task.type=="todo"', ng-class='{"group-yesno": !!obj.leader}')
|
||||||
input.task-input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task._id}}', type='checkbox',
|
input.task-input.visuallyhidden.focusable(id='box-{{::obj._id}}_{{::task._id}}', type='checkbox',
|
||||||
ng-model='task.completed', ng-if='$state.includes("tasks")',
|
ng-model='task.completed', ng-if='$state.includes("tasks")',
|
||||||
ng-change='changeCheck(task)'
|
ng-change='changeCheck(task)'
|
||||||
|
|||||||
Reference in New Issue
Block a user