mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +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",
|
||||
"sinon": "^1.17.2",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"superagent-defaults": "^0.1.13",
|
||||
"vinyl-transform": "^1.0.0",
|
||||
"webpack-dev-middleware": "^1.4.0",
|
||||
|
||||
@@ -69,4 +69,16 @@ describe('DELETE /tasks/:id', () => {
|
||||
expect(syncedTask.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();
|
||||
|
||||
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].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.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}`);
|
||||
|
||||
let groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
@@ -93,6 +93,14 @@ describe('POST /tasks/:taskId', () => {
|
||||
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 () => {
|
||||
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 { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import stripeModule from 'stripe';
|
||||
import moment from 'moment';
|
||||
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper.js';
|
||||
import i18n from '../../../../../website/common/script/i18n';
|
||||
import amzLib from '../../../../../website/server/libs/amazonPayments';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user, group, data, plan;
|
||||
|
||||
let stripe = stripeModule('test');
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
@@ -625,7 +630,40 @@ describe('payments/index', () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
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 { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { BadRequest } from '../../../../../website/server/libs/errors';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../../../../../website/server/libs/errors';
|
||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import validator from 'validator';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import shared from '../../../../../website/common';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
|
||||
@@ -638,6 +641,22 @@ describe('Group Model', () => {
|
||||
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 () => {
|
||||
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 User } from '../../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../../website/server/models/task';
|
||||
import { each, find } from 'lodash';
|
||||
import { each, find, findIndex } from 'lodash';
|
||||
|
||||
describe('Group Task Methods', () => {
|
||||
let guild, leader, challenge, task;
|
||||
@@ -68,11 +68,29 @@ describe('Group Task Methods', () => {
|
||||
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
|
||||
task.group.id = guild._id;
|
||||
await task.save();
|
||||
if (task.checklist) {
|
||||
task.checklist.push({
|
||||
text: 'Checklist Item 1',
|
||||
completed: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('syncs an assigned task to a user', async () => {
|
||||
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 updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
|
||||
let syncedTask = find(updatedLeadersTasks, findLinkedTask);
|
||||
@@ -96,38 +114,124 @@ describe('Group Task Methods', () => {
|
||||
expect(syncedTask.text).to.equal(task.text);
|
||||
});
|
||||
|
||||
it('syncs updated info for assigned task to all users', async () => {
|
||||
let newMember = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
await newMember.save();
|
||||
|
||||
it('syncs checklist items to an assigned user', async () => {
|
||||
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 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);
|
||||
if (task.type !== 'daily' && task.type !== 'todo') return;
|
||||
|
||||
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(syncedTask.checklist.length).to.equal(task.checklist.length);
|
||||
expect(syncedTask.checklist[0].text).to.equal(task.checklist[0].text);
|
||||
});
|
||||
|
||||
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);
|
||||
describe('syncs updated info', async() => {
|
||||
let newMember;
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -275,7 +275,8 @@ describe('Challenges Controller', function() {
|
||||
describe('editTask', function() {
|
||||
it('is Tasks.editTask', function() {
|
||||
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() {
|
||||
it('is Tasks.editTask', function() {
|
||||
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 = {};
|
||||
tasks = Tasks;
|
||||
});
|
||||
|
||||
rootScope.openModal = function () {};
|
||||
});
|
||||
|
||||
it('calls get user tasks endpoint', function() {
|
||||
@@ -151,7 +153,6 @@ describe('Tasks Service', function() {
|
||||
});
|
||||
|
||||
describe('editTask', function() {
|
||||
|
||||
var task;
|
||||
|
||||
beforeEach(function(){
|
||||
|
||||
@@ -157,6 +157,16 @@ describe('shared.ops.scoreTask', () => {
|
||||
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', () => {
|
||||
it('up', () => {
|
||||
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'));
|
||||
global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
let sinonStubPromise = require('sinon-stub-promise');
|
||||
sinonStubPromise(global.sinon);
|
||||
global.sandbox = sinon.sandbox.create();
|
||||
global.Promise = Bluebird;
|
||||
|
||||
@@ -8,59 +8,67 @@
|
||||
// array of keywords and their associated color vars
|
||||
$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 $stage in $stages
|
||||
.task-column:not(.rewards)
|
||||
.color-{$stage[0]}:not(.completed)
|
||||
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;
|
||||
taskContainerStyles($stage)
|
||||
|
||||
.task-modal
|
||||
&.color-{$stage[0]}:not(.completed)
|
||||
taskContainerStyles($stage)
|
||||
|
||||
// completed has to be outside the loop to override the color class
|
||||
.completed
|
||||
@@ -366,6 +374,12 @@ for $stage in $stages
|
||||
text-align: center
|
||||
opacity: 0.75
|
||||
|
||||
// Group yesno
|
||||
.group-yesno
|
||||
label:hover:after, input[type=checkbox]:checked + label:after
|
||||
content: "" !important
|
||||
opacity: 1 !important
|
||||
|
||||
|
||||
// secondary task commands
|
||||
// -----------------------
|
||||
|
||||
@@ -139,6 +139,13 @@ window.habitrpg = angular.module('habitrpg',
|
||||
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', {
|
||||
url: '/guilds',
|
||||
templateUrl: "partials/options.social.guilds.html",
|
||||
@@ -155,38 +162,55 @@ window.habitrpg = angular.module('habitrpg',
|
||||
templateUrl: "partials/options.social.guilds.create.html",
|
||||
title: env.t('titleGuilds')
|
||||
})
|
||||
|
||||
.state('options.social.guilds.detail', {
|
||||
url: '/:gid',
|
||||
templateUrl: 'partials/options.social.guilds.detail.html',
|
||||
title: env.t('titleGuilds'),
|
||||
controller: ['$scope', 'Groups', 'Chat', '$stateParams', 'Members', 'Challenges', 'Tasks',
|
||||
function($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, User, $location) {
|
||||
$scope.groupPanel = 'chat';
|
||||
$scope.upgrade = false;
|
||||
|
||||
// @TODO: Move this to service or single http request
|
||||
Groups.Group.get($stateParams.gid)
|
||||
.then(function (response) {
|
||||
$scope.obj = $scope.group = response.data.data;
|
||||
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);
|
||||
// })
|
||||
// });
|
||||
return Chat.markChatSeen($scope.group._id);
|
||||
})
|
||||
.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',
|
||||
function ($scope, Tasks) {
|
||||
$scope.approvals = [];
|
||||
|
||||
// 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;
|
||||
$scope.approve = function (taskId, userId, username, $index) {
|
||||
if (!confirm(env.t('confirmTaskApproval', {username: username}))) return;
|
||||
Tasks.approve(taskId, userId)
|
||||
.then(function (response) {
|
||||
$scope.approvals.splice($index, 1);
|
||||
$scope.group.approvals.splice($index, 1);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.approvalTitle = function (approval) {
|
||||
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 = [];
|
||||
_.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', {
|
||||
tags: currentTags,
|
||||
allowedTags: currentTags,
|
||||
allowedTags: allowedTags,
|
||||
allowDuplicates: false,
|
||||
preserveCase: true,
|
||||
placeholder: window.env.t('assignFieldPlaceholder'),
|
||||
onBeforeTagAdd: function(event, tag) {
|
||||
return confirm(window.env.t('confirmAddTag', {tag: tag}));
|
||||
},
|
||||
|
||||
@@ -1,17 +1,65 @@
|
||||
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.cancelTaskEdit = Tasks.cancelTaskEdit;
|
||||
|
||||
$scope.editTask = function (task, user, taskStatus) {
|
||||
Tasks.editTask(task, user, taskStatus, $scope);
|
||||
};
|
||||
|
||||
function addTask (listDef, taskTexts) {
|
||||
taskTexts.forEach(function (taskText) {
|
||||
var task = Shared.taskDefaults({text: taskText, type: listDef.type});
|
||||
|
||||
//If the group has not been created, we bulk add tasks on save
|
||||
var group = $scope.obj;
|
||||
if (group._id) Tasks.createGroupTasks(group._id, task);
|
||||
if (!group[task.type + 's']) group[task.type + 's'] = [];
|
||||
group[task.type + 's'].unshift(task);
|
||||
if (!group._id) return;
|
||||
|
||||
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) {
|
||||
Tasks.saveTask (task, stayOpen, isSaveAndClose);
|
||||
Tasks.updateTask(task._id, task);
|
||||
// Check if we have a lingering checklist that the enter button did not trigger on
|
||||
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){
|
||||
@@ -63,9 +124,23 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', func
|
||||
*/
|
||||
$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;
|
||||
|
||||
@@ -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
|
||||
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;
|
||||
}
|
||||
|
||||
$scope.editTask = Tasks.editTask;
|
||||
$scope.editTask = function (task, user, taskStatus) {
|
||||
Tasks.editTask(task, user, taskStatus, $scope);
|
||||
};
|
||||
|
||||
$scope.cancelTaskEdit = Tasks.cancelTaskEdit;
|
||||
|
||||
$scope.canEdit = function(task) {
|
||||
@@ -313,6 +316,15 @@ habitrpg.controller("ChallengesCtrl", ['$rootScope','$scope', 'Shared', 'User',
|
||||
|
||||
$scope.toggleBulk = Tasks.toggleBulk;
|
||||
|
||||
/*
|
||||
* Task Details
|
||||
*/
|
||||
$scope.taskPopover = function (task) {
|
||||
if (task.popoverOpen) return '';
|
||||
var content = task.notes;
|
||||
return content;
|
||||
};
|
||||
|
||||
/*
|
||||
--------------------------
|
||||
Subscription
|
||||
|
||||
@@ -45,4 +45,8 @@ habitrpg.controller("FiltersCtrl", ['$scope', '$rootScope', 'User', 'Shared',
|
||||
User.addTag({body:{name: $scope._newTag.name, id: Shared.uuid()}});
|
||||
$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});
|
||||
};
|
||||
}]);
|
||||
@@ -34,7 +34,7 @@ habitrpg.controller("GuildsCtrl", ['$scope', 'Groups', 'User', 'Challenges', '$r
|
||||
Groups.Group.create(group)
|
||||
.then(function (response) {
|
||||
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) {
|
||||
var user = $scope.user;
|
||||
|
||||
if (user.purchased && user.purchased.plan && user.purchased.plan.mysteryItems && user.purchased.plan.mysteryItems.length) {
|
||||
return mysteryValue;
|
||||
} else if ((user.invitations.party && user.invitations.party.id) || (user.invitations.guilds && user.invitations.guilds.length > 0)) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
habitrpg.controller('NotificationCtrl',
|
||||
['$scope', '$rootScope', 'Shared', 'Content', 'User', 'Guide', 'Notification', 'Analytics', 'Achievement', 'Social',
|
||||
function ($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, Tasks) {
|
||||
|
||||
$rootScope.$watch('user.stats.hp', function (after, before) {
|
||||
if (after <= 0){
|
||||
@@ -87,6 +87,8 @@ habitrpg.controller('NotificationCtrl',
|
||||
if (!after || after.length === 0) return;
|
||||
|
||||
var notificationsToRead = [];
|
||||
var scoreTaskNotification;
|
||||
|
||||
after.forEach(function (notification) {
|
||||
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||
return;
|
||||
@@ -141,6 +143,9 @@ habitrpg.controller('NotificationCtrl',
|
||||
trasnferGroupNotification(notification);
|
||||
markAsRead = false;
|
||||
break;
|
||||
case 'SCORED_TASK':
|
||||
scoreTaskNotification = notification;
|
||||
break;
|
||||
case 'LOGIN_INCENTIVE':
|
||||
Notification.showLoginIncentive(User.user, notification.data, Social.loadWidgets);
|
||||
break;
|
||||
@@ -159,7 +164,17 @@ habitrpg.controller('NotificationCtrl',
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ habitrpg.controller("PartyCtrl", ['$rootScope','$scope','Groups','Chat','User','
|
||||
$scope.type = 'party';
|
||||
$scope.text = window.env.t('party');
|
||||
$scope.group = {loadingParty: true};
|
||||
$scope.groupPanel = 'chat';
|
||||
|
||||
$scope.inviteOrStartParty = Groups.inviteOrStartParty;
|
||||
$scope.loadWidgets = Social.loadWidgets;
|
||||
|
||||
@@ -51,11 +51,17 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
||||
|
||||
$scope.toggleBulk = Tasks.toggleBulk;
|
||||
|
||||
$scope.editTask = Tasks.editTask;
|
||||
$scope.editTask = function (task, user, taskStatus) {
|
||||
Tasks.editTask(task, user, taskStatus, $scope);
|
||||
};
|
||||
|
||||
$scope.canEdit = function(task) {
|
||||
// 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) {
|
||||
@@ -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
|
||||
return true;
|
||||
|
||||
var shouldDo = task.type == 'daily' ? habitrpgShared.shouldDo(new Date, task, prefs) : true;
|
||||
switch (list.view) {
|
||||
case "yellowred": // Habits
|
||||
@@ -324,4 +331,13 @@ habitrpg.controller("TasksCtrl", ['$scope', '$rootScope', '$location', 'User','N
|
||||
|
||||
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'),
|
||||
token: function(res) {
|
||||
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.subscription) url += '&sub='+sub.key;
|
||||
if (data.coupon) url += '&coupon='+data.coupon;
|
||||
if (data.groupId) url += '&groupId=' + data.groupId;
|
||||
$http.post(url, res).success(function() {
|
||||
window.location.reload(true);
|
||||
$http.post(url, res).success(function(response) {
|
||||
if (response && response.data && response.data._id) {
|
||||
$rootScope.hardRedirect('/#/options/groups/guilds/' + response.data._id);
|
||||
} else {
|
||||
window.location.reload(true);
|
||||
}
|
||||
}).error(function(res) {
|
||||
alert(res.message);
|
||||
});
|
||||
@@ -116,6 +127,10 @@ function($rootScope, User, $http, Content) {
|
||||
Payments.amazonPayments.groupId = data.groupId;
|
||||
}
|
||||
|
||||
if (data.groupToCreate) {
|
||||
Payments.amazonPayments.groupToCreate = data.groupToCreate;
|
||||
}
|
||||
|
||||
Payments.amazonPayments.gift = data.gift;
|
||||
Payments.amazonPayments.type = data.type;
|
||||
|
||||
@@ -255,14 +270,24 @@ function($rootScope, User, $http, Content) {
|
||||
} else if(Payments.amazonPayments.type === 'subscription') {
|
||||
var url = '/amazon/subscribe';
|
||||
|
||||
if (Payments.amazonPayments.groupToCreate) {
|
||||
url = '/api/v3/groups/create-plan';
|
||||
}
|
||||
|
||||
$http.post(url, {
|
||||
billingAgreementId: Payments.amazonPayments.billingAgreementId,
|
||||
subscription: Payments.amazonPayments.subscription,
|
||||
coupon: Payments.amazonPayments.coupon,
|
||||
groupId: Payments.amazonPayments.groupId,
|
||||
}).success(function(){
|
||||
groupToCreate: Payments.amazonPayments.groupToCreate,
|
||||
paymentType: 'Amazon',
|
||||
}).success(function(response) {
|
||||
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){
|
||||
alert(res.message);
|
||||
Payments.amazonPayments.reset();
|
||||
|
||||
@@ -245,12 +245,31 @@ angular.module('habitrpg')
|
||||
});
|
||||
};
|
||||
|
||||
function editTask(task, user) {
|
||||
task._editing = true;
|
||||
task._tags = !user.preferences.tagsCollapsed;
|
||||
task._advanced = !user.preferences.advancedCollapsed;
|
||||
task._edit = angular.copy(task);
|
||||
function editTask(task, user, taskStatus, scopeInc) {
|
||||
var modalScope = $rootScope.$new();
|
||||
modalScope.task = task;
|
||||
modalScope.task._editing = true;
|
||||
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;
|
||||
|
||||
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) {
|
||||
@@ -289,7 +308,7 @@ angular.module('habitrpg')
|
||||
|
||||
function focusChecklist(task, index) {
|
||||
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) {
|
||||
UserNotifications.readNotifications(notificationIds);
|
||||
return UserNotifications.readNotifications(notificationIds);
|
||||
},
|
||||
|
||||
addTag: function(data) {
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
"js/controllers/tavernCtrl.js",
|
||||
"js/controllers/tasksCtrl.js",
|
||||
"js/controllers/userCtrl.js",
|
||||
"js/controllers/groupPlansCtrl.js",
|
||||
|
||||
"js/components/groupTasks/groupTasksController.js",
|
||||
"js/components/groupTasks/groupTasksDirective.js",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"help": "Help",
|
||||
"user": "User",
|
||||
"market": "Market",
|
||||
"groupPlansTitle": "Group Plans",
|
||||
"subscriberItem": "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.",
|
||||
@@ -194,5 +195,7 @@
|
||||
"you": "(you)",
|
||||
"enableDesktopNotifications": "Enable Desktop Notifications",
|
||||
"online": "online",
|
||||
"onlineCount": "<%= count %> online"
|
||||
"onlineCount": "<%= count %> online",
|
||||
"loading": "Loading...",
|
||||
"userIdRequired": "User ID is required"
|
||||
}
|
||||
|
||||
@@ -113,7 +113,9 @@
|
||||
"toUserIDRequired": "A User ID is required",
|
||||
"gemAmountRequired": "A number of gems is required",
|
||||
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
|
||||
"privateMessageGiftGemsMessage": "Hello <%= receiverName %>, <%= senderName %> has sent you <%= gemAmount %> gems!",
|
||||
"privateMessageGiftIntro": "Hello <%= receiverName %>, <%= senderName %> has sent you ",
|
||||
"privateMessageGiftGemsMessage": "<%= gemAmount %> gems!",
|
||||
"privateMessageGiftSubscriptionMessage": "<%= numberOfMonths %> months of subscription! ",
|
||||
"cannotSendGemsToYourself": "Cannot send gems to yourself. Try a subscription instead.",
|
||||
"badAmountOfGemsToSend": "Amount must be within 1 and your current number of gems.",
|
||||
"abuseFlag": "Report violation of Community Guidelines",
|
||||
@@ -170,8 +172,8 @@
|
||||
"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.",
|
||||
"partyUpName": "Party Up",
|
||||
"partyOnName": "Party On",
|
||||
"partyUpText": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
|
||||
"partyOnText": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
|
||||
"partyUpAchievement": "Joined a Party with another person! Have fun battling monsters and supporting each other.",
|
||||
"partyOnAchievement": "Joined a Party with at least four people! Enjoy your increased accountability as you unite with your friends to vanquish your foes!",
|
||||
"largeGroupNote": "Note: This Guild is now too large to support notifications! Be sure to check back every day to see new messages.",
|
||||
"groupIdRequired": "\"groupId\" must be a valid UUID",
|
||||
"groupNotFound": "Group not found or you don't have access.",
|
||||
@@ -210,15 +212,46 @@
|
||||
"to": "To:",
|
||||
"from": "From:",
|
||||
"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.",
|
||||
"confirmAddTag": "Do you really want to add \"<%= tag %>\"?",
|
||||
"confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
|
||||
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
|
||||
|
||||
"groupHomeTitle": "Home",
|
||||
"assignTask": "Assign Task",
|
||||
"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.",
|
||||
"claim": "Claim",
|
||||
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
|
||||
"yourTaskHasBeenApproved": "Your task has been approved",
|
||||
"yourTaskHasBeenApproved": "Your task \"<%= taskText %>\" has been approved",
|
||||
"userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
|
||||
"confirmTaskApproval": "Are you sure you want to approve this task?",
|
||||
"approve": "Approve",
|
||||
"approvalTitle": "<%= text %> for user: <%= userName %>"
|
||||
"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 backgrounds from './appearance/backgrounds.js'
|
||||
import spells from './spells';
|
||||
import subscriptionBlocks from './subscriptionBlocks';
|
||||
import faq from './faq';
|
||||
import timeTravelers from './time-travelers';
|
||||
|
||||
@@ -33,6 +34,7 @@ api.itemList = ITEM_LIST;
|
||||
|
||||
api.gear = gear;
|
||||
api.spells = spells;
|
||||
api.subscriptionBlocks = subscriptionBlocks;
|
||||
|
||||
api.mystery = timeTravelers.mystery;
|
||||
api.timeTravelerStore = timeTravelers.timeTravelerStore;
|
||||
@@ -2808,35 +2810,6 @@ api.appearances = appearances;
|
||||
|
||||
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 = {
|
||||
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,
|
||||
};
|
||||
|
||||
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
|
||||
// the API consumer, then cleared afterwards
|
||||
user._tmp = {};
|
||||
|
||||
@@ -21,6 +21,9 @@ import { encrypt } from '../../libs/encryption';
|
||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||
import pusher from '../../libs/pusher';
|
||||
import common from '../../../common';
|
||||
import payments from '../../libs/payments';
|
||||
import shared from '../../../common';
|
||||
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @apiName GetGroups
|
||||
@@ -303,6 +395,8 @@ api.joinGroup = {
|
||||
|
||||
group.memberCount += 1;
|
||||
|
||||
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
|
||||
let promises = [group.save(), user.save()];
|
||||
|
||||
if (inviter) {
|
||||
@@ -459,6 +553,8 @@ api.leaveGroup = {
|
||||
|
||||
await group.leave(user, req.query.keep);
|
||||
|
||||
if (group.purchased.plan && group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
|
||||
_removeMessagesFromMember(user, group._id);
|
||||
|
||||
await user.save();
|
||||
@@ -535,6 +631,7 @@ api.removeGroupMember = {
|
||||
|
||||
if (isInGroup) {
|
||||
group.memberCount -= 1;
|
||||
if (group.purchased.plan.customerId) await payments.updateStripeGroupPlan(group);
|
||||
|
||||
if (group.quest && group.quest.leader === member._id) {
|
||||
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
|
||||
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?
|
||||
let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
|
||||
|
||||
@@ -254,6 +254,8 @@ api.updateTask = {
|
||||
|
||||
if (!challenge && task.userId && task.challenge && task.challenge.id) {
|
||||
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
|
||||
} else if (!group && task.userId && task.group && task.group.id) {
|
||||
sanitizedObj = Tasks.Task.sanitizeUserChallengeTask(updatedTaskObj);
|
||||
} else {
|
||||
sanitizedObj = Tasks.Task.sanitize(updatedTaskObj);
|
||||
}
|
||||
@@ -270,7 +272,15 @@ api.updateTask = {
|
||||
let savedTask = await task.save();
|
||||
|
||||
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);
|
||||
@@ -508,13 +518,16 @@ api.addChecklistItem = {
|
||||
|
||||
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();
|
||||
|
||||
newCheckListItem.id = savedTask.checklist[savedTask.checklist.length - 1].id;
|
||||
|
||||
res.respond(200, savedTask);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
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);
|
||||
if (challenge) challenge.updateTask(savedTask);
|
||||
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'));
|
||||
} else if (task.userId && task.challenge.id && !task.challenge.broken) {
|
||||
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) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import ensureDevelpmentMode from '../../../middlewares/ensureDevelpmentMode';
|
||||
import Bluebird from 'bluebird';
|
||||
import * as Tasks from '../../../models/task';
|
||||
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.
|
||||
* @apiName CreateGroupTasks
|
||||
* @apiGroup Task
|
||||
* @apiIgnore
|
||||
*
|
||||
* @apiParam {UUID} groupId The id of the group the new task(s) will belong to
|
||||
*
|
||||
@@ -31,7 +29,7 @@ let api = {};
|
||||
api.createGroupTasks = {
|
||||
method: 'POST',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
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
|
||||
* @apiName GetGroupTasks
|
||||
* @apiGroup Task
|
||||
* @apiIgnore
|
||||
*
|
||||
* @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
|
||||
@@ -65,7 +62,7 @@ api.createGroupTasks = {
|
||||
api.getGroupTasks = {
|
||||
method: 'GET',
|
||||
url: '/tasks/group/:groupId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||
@@ -97,7 +94,7 @@ api.getGroupTasks = {
|
||||
api.assignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/assign/:assignedUserId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -120,12 +117,22 @@ api.assignTask = {
|
||||
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.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);
|
||||
},
|
||||
@@ -145,7 +152,7 @@ api.assignTask = {
|
||||
api.unassignTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/unassign/:assignedUserId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('assignedUserId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -194,7 +201,7 @@ api.unassignTask = {
|
||||
api.approveTask = {
|
||||
method: 'POST',
|
||||
url: '/tasks/:taskId/approve/:userId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('taskId', res.t('taskIdRequired')).notEmpty().isUUID();
|
||||
req.checkParams('userId', res.t('userIdRequired')).notEmpty().isUUID();
|
||||
@@ -226,10 +233,15 @@ api.approveTask = {
|
||||
task.group.approval.approved = true;
|
||||
|
||||
assignedUser.addNotification('GROUP_TASK_APPROVED', {
|
||||
message: res.t('yourTaskHasBeenApproved'),
|
||||
message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
|
||||
groupId: group._id,
|
||||
});
|
||||
|
||||
assignedUser.addNotification('SCORED_TASK', {
|
||||
message: res.t('yourTaskHasBeenApproved', {taskText: task.text}),
|
||||
scoreTask: task,
|
||||
});
|
||||
|
||||
await Bluebird.all([assignedUser.save(), task.save()]);
|
||||
|
||||
res.respond(200, task);
|
||||
@@ -241,7 +253,6 @@ api.approveTask = {
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetGroupApprovals
|
||||
* @apiGroup Task
|
||||
* @apiIgnore
|
||||
*
|
||||
* @apiParam {UUID} groupId The id of the group from which to retrieve the approvals
|
||||
*
|
||||
@@ -250,7 +261,7 @@ api.approveTask = {
|
||||
api.getGroupApprovals = {
|
||||
method: 'GET',
|
||||
url: '/approvals/group/:groupId',
|
||||
middlewares: [ensureDevelpmentMode, authWithHeaders()],
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ api.checkout = {
|
||||
let orderReferenceId = req.body.orderReferenceId;
|
||||
let amount = 5;
|
||||
|
||||
// @TODO: Make thise use payment.subscribeWithAmazon
|
||||
|
||||
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
|
||||
|
||||
if (gift) {
|
||||
|
||||
@@ -49,6 +49,9 @@ api.checkout = {
|
||||
let groupId = req.query.groupId;
|
||||
let coupon;
|
||||
let response;
|
||||
let subscriptionId;
|
||||
|
||||
// @TODO: Update this to use payments.payWithStripe
|
||||
|
||||
if (!token) throw new BadRequest('Missing req.body.id');
|
||||
|
||||
@@ -59,12 +62,20 @@ api.checkout = {
|
||||
if (!coupon) throw new BadRequest(res.t('invalidCoupon'));
|
||||
}
|
||||
|
||||
response = await stripe.customers.create({
|
||||
let customerObject = {
|
||||
email: req.body.email,
|
||||
metadata: { uuid: user._id },
|
||||
card: token,
|
||||
plan: sub.key,
|
||||
});
|
||||
};
|
||||
|
||||
if (groupId) {
|
||||
customerObject.quantity = sub.quantity;
|
||||
}
|
||||
|
||||
response = await stripe.customers.create(customerObject);
|
||||
|
||||
if (groupId) subscriptionId = response.subscriptions.data[0].id;
|
||||
} else {
|
||||
let amount = 500; // $5
|
||||
|
||||
@@ -91,6 +102,7 @@ api.checkout = {
|
||||
sub,
|
||||
headers: req.headers,
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
} else {
|
||||
let method = 'buyGems';
|
||||
@@ -149,7 +161,9 @@ api.subscribeEdit = {
|
||||
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'));
|
||||
}
|
||||
customerId = group.purchased.plan.customerId;
|
||||
|
||||
@@ -11,11 +11,23 @@ import {
|
||||
model as Group,
|
||||
basicFields as basicGroupFields,
|
||||
} from '../models/group';
|
||||
import { model as Coupon } from '../models/coupon';
|
||||
import { model as User } from '../models/user';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from './errors';
|
||||
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 = {};
|
||||
|
||||
@@ -49,6 +61,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
let groupId;
|
||||
let itemPurchased = 'Subscription';
|
||||
let purchaseType = 'subscribe';
|
||||
let emailType = 'subscription-begins';
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
@@ -66,7 +79,9 @@ api.createSubscription = async function createSubscription (data) {
|
||||
recipient = group;
|
||||
itemPurchased = 'Group-Subscription';
|
||||
purchaseType = 'group-subscribe';
|
||||
emailType = 'group-subscription-begins';
|
||||
groupId = group._id;
|
||||
recipient.purchased.plan.quantity = data.sub.quantity;
|
||||
}
|
||||
|
||||
plan = recipient.purchased.plan;
|
||||
@@ -98,11 +113,16 @@ api.createSubscription = async function createSubscription (data) {
|
||||
// Specify a lastBillingDate just for Amazon Payments
|
||||
// Resetted every time the subscription restarts
|
||||
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? today : undefined,
|
||||
owner: data.user._id,
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
gemsBought: 0,
|
||||
dateCreated: today,
|
||||
mysteryItems: [],
|
||||
}).value();
|
||||
|
||||
if (data.subscriptionId) {
|
||||
plan.subscriptionId = data.subscriptionId;
|
||||
}
|
||||
}
|
||||
|
||||
// Block sub perks
|
||||
@@ -119,7 +139,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
}
|
||||
|
||||
if (!data.gift) {
|
||||
txnEmail(data.user, 'subscription-begins');
|
||||
txnEmail(data.user, emailType);
|
||||
}
|
||||
|
||||
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
|
||||
api.cancelSubscription = async function cancelSubscription (data) {
|
||||
let plan;
|
||||
let group;
|
||||
let cancelType = 'unsubscribe';
|
||||
let groupId;
|
||||
let emailType = 'cancel-subscription';
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
@@ -224,10 +261,13 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
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'));
|
||||
}
|
||||
plan = group.purchased.plan;
|
||||
emailType = 'group-cancel-subscription';
|
||||
} else {
|
||||
plan = data.user.purchased.plan;
|
||||
}
|
||||
@@ -252,7 +292,7 @@ api.cancelSubscription = async function cancelSubscription (data) {
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
txnEmail(data.user, 'cancel-subscription');
|
||||
txnEmail(data.user, emailType);
|
||||
|
||||
if (group) {
|
||||
cancelType = 'group-unsubscribe';
|
||||
@@ -343,4 +383,180 @@ api.buyGems = async function buyGems (data) {
|
||||
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;
|
||||
|
||||
@@ -137,6 +137,7 @@ async function cronAsync (req, res) {
|
||||
|
||||
// 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 group completed todos
|
||||
Tasks.Task.remove({
|
||||
userId: user._id,
|
||||
type: 'todo',
|
||||
@@ -145,6 +146,7 @@ async function cronAsync (req, res) {
|
||||
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
|
||||
},
|
||||
'challenge.id': {$exists: false},
|
||||
'group.id': {$exists: false},
|
||||
}).exec();
|
||||
|
||||
res.locals.wasModified = true; // TODO remove after v2 is retired
|
||||
|
||||
@@ -13,6 +13,7 @@ import { groupChatReceivedWebhook } from '../libs/webhook';
|
||||
import {
|
||||
InternalServerError,
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
import baseModel from '../libs/baseModel';
|
||||
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 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({
|
||||
_id: {$in: user.challenges},
|
||||
group: group._id,
|
||||
@@ -899,6 +905,13 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
promises.push(group.remove());
|
||||
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)
|
||||
update.$inc = {memberCount: -1};
|
||||
@@ -915,7 +928,16 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
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 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.assignedUsers'] = taskToSync.group.assignedUsers;
|
||||
|
||||
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},
|
||||
'group.id': group.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) {
|
||||
@@ -952,11 +1011,13 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
|
||||
if (userTags[i].name !== group.name) {
|
||||
// update the name - it's been changed since
|
||||
userTags[i].name = group.name;
|
||||
userTags[i].group = group._id;
|
||||
}
|
||||
} else {
|
||||
userTags.push({
|
||||
id: group._id,
|
||||
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.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.tags.indexOf(group._id) === -1) matchingTask.tags.push(group._id); // add tag if missing
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import mongoose from 'mongoose';
|
||||
import baseModel from '../libs/baseModel';
|
||||
import validator from 'validator';
|
||||
|
||||
export let schema = new mongoose.Schema({
|
||||
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', '']}
|
||||
customerId: String, // Billing Agreement Id in case of Amazon Payments
|
||||
dateCreated: Date,
|
||||
|
||||
@@ -14,6 +14,7 @@ export let schema = new Schema({
|
||||
},
|
||||
name: {type: String, required: true},
|
||||
challenge: {type: String},
|
||||
group: {type: String},
|
||||
}, {
|
||||
strict: true,
|
||||
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
|
||||
_id: false,
|
||||
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',
|
||||
'LOGIN_INCENTIVE',
|
||||
'GROUP_INVITE_ACCEPTED',
|
||||
'SCORED_TASK',
|
||||
];
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
@@ -34,8 +34,9 @@
|
||||
button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})')
|
||||
span.glyphicon.glyphicon-trash
|
||||
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)')
|
||||
span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge")
|
||||
markdown(text='tag.name')
|
||||
| {{tag}}
|
||||
// <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}">
|
||||
|
||||
@@ -367,64 +367,4 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
|
||||
|
||||
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)')
|
||||
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')
|
||||
include ./settings/subscription
|
||||
|
||||
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')
|
||||
// <span class="task-action-btn tile flush bright add-gems-btn">+</span>
|
||||
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')
|
||||
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-4
|
||||
ul.options-menu(ng-show='group.privacy === "private"')
|
||||
li
|
||||
a(ng-click="groupPanel = 'chat'")=env.t('groupHomeTitle')
|
||||
li(ng-show='group.purchased.active')
|
||||
a(ng-click="groupPanel = 'tasks'")=env.t('groupTasksTitle')
|
||||
li(ng-show='group.purchased.active && group.leader._id === user._id')
|
||||
a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
|
||||
li
|
||||
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('subscription')
|
||||
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && !group.purchased.plan.customerId')=env.t('upgradeTitle')
|
||||
|
||||
// ------ Bosses -------
|
||||
+boss(false, false)
|
||||
.tab-content
|
||||
.tab-pane.active
|
||||
|
||||
// ------ Information -------
|
||||
.panel.panel-default
|
||||
.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')
|
||||
.row(ng-show="groupPanel == 'chat'")
|
||||
.col-md-4
|
||||
|
||||
.panel-body
|
||||
form(ng-show='group._editing')
|
||||
// ------ Bosses -------
|
||||
+boss(false, false)
|
||||
|
||||
// ------ Information -------
|
||||
.panel.panel-default
|
||||
.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')
|
||||
|
||||
.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
|
||||
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')
|
||||
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})'
|
||||
)
|
||||
|
||||
h4=env.t('assignLeader')
|
||||
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
|
||||
include ./challenge-box
|
||||
|
||||
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}}
|
||||
div(ng-if="group")
|
||||
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
||||
|
||||
.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')
|
||||
.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}}
|
||||
|
||||
// ------ 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')
|
||||
.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
|
||||
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}}
|
||||
.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')
|
||||
|
||||
.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})'
|
||||
)
|
||||
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
|
||||
|
||||
include ./challenge-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')
|
||||
|
||||
div(ng-if="group")
|
||||
a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
|
||||
group-tasks(ng-show="groupPanel == 'tasks'")
|
||||
|
||||
.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}}
|
||||
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
|
||||
|
||||
.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')
|
||||
+groupSubscription
|
||||
|
||||
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-pane.active
|
||||
|
||||
div(ng-controller='ChatCtrl', ng-show="groupPane == 'chat'")
|
||||
.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="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')
|
||||
.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')
|
||||
|
||||
.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'")
|
||||
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, groupId: group.id})', 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 : ""}}&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'))
|
||||
.col-xs-4
|
||||
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'))
|
||||
|
||||
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')
|
||||
.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-heading
|
||||
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
|
||||
|
||||
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 ./party
|
||||
include ./groups/group-tasks
|
||||
include ./groups/group-plans
|
||||
include ./groups/create-group
|
||||
|
||||
script(type='text/ng-template', id='partials/options.social.inbox.html')
|
||||
.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')
|
||||
div.col-xs-12
|
||||
include ./create-group
|
||||
+groupCreateForm
|
||||
|
||||
script(type='text/ng-template', id='partials/options.social.guilds.html')
|
||||
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') }")
|
||||
a(ui-sref='options.social.hall.heroes')
|
||||
=env.t('hall')
|
||||
li(ng-class="{ active: $state.includes('options.social.groupPlans') }")
|
||||
a(ui-sref='options.social.groupPlans')
|
||||
=env.t('groupPlansTitle')
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active
|
||||
|
||||
@@ -36,6 +36,8 @@ nav.toolbar(ng-controller='MenuCtrl')
|
||||
a(ui-sref='options.social.challenges')=env.t('challenges')
|
||||
li
|
||||
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
||||
li
|
||||
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
|
||||
ul.toolbar-submenu
|
||||
li
|
||||
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')
|
||||
li
|
||||
a(ui-sref='options.social.hall.heroes')=env.t('hall')
|
||||
li
|
||||
a(ui-sref='options.social.groupPlans')=env.t('groupPlansTitle')
|
||||
li.toolbar-button-dropdown
|
||||
a(ui-sref='options.inventory.drops', data-close-menu)
|
||||
span=env.t('inventory')
|
||||
|
||||
@@ -24,6 +24,7 @@ include ./enable-desktop-notifications.jade
|
||||
include ./login-incentives.jade
|
||||
include ./login-incentives-reward-unlocked.jade
|
||||
include ./generic.jade
|
||||
include ./tasks-edit.jade
|
||||
|
||||
//- Settings
|
||||
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,5 +1,6 @@
|
||||
div(ng-if='task._editing')
|
||||
.task-options
|
||||
h2 {{task._edit.text}}
|
||||
|
||||
// Broken Challenge
|
||||
.well(ng-if='task.challenge.broken')
|
||||
@@ -33,7 +34,7 @@ div(ng-if='task._editing')
|
||||
|
||||
include ./checklist
|
||||
|
||||
form(ng-submit='saveTask(task,false,true)')
|
||||
form
|
||||
include ./text_notes
|
||||
|
||||
include ./habits/plus_minus
|
||||
@@ -49,4 +50,11 @@ div(ng-if='task._editing')
|
||||
include ./advanced_options
|
||||
|
||||
.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')
|
||||
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)")
|
||||
markdown(text='tag.name')
|
||||
|
||||
@@ -6,7 +6,7 @@ include ./task_view/mixins
|
||||
script(id='templates/habitrpg-tasks.html', type="text/ng-template")
|
||||
.tasks-lists.container-fluid
|
||||
.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')
|
||||
|
||||
include ./task_view/graph
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
span(ng-if='task.type=="todo" && task.date')
|
||||
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}}
|
||||
|
||||
// Approval requested
|
||||
|
|
||||
span(ng-show='task.group.approval.requested && !task.group.approval.approved')
|
||||
span(tooltip=env.t('approvalRequested'))
|
||||
span=env.t('approvalRequested')
|
||||
|
|
||||
|
||||
// Streak
|
||||
|
|
||||
span(ng-show='task.streak') {{task.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'))
|
||||
|{{checklistCompletion(task.checklist)}}/{{task.checklist.length}}
|
||||
span.glyphicon.glyphicon-tags(tooltip='{{Shared.appliedTags(user.tags, task.tags)}}', ng-hide='Shared.noTags(task.tags)')
|
||||
|
||||
// edit
|
||||
a(ng-hide='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')
|
||||
|
|
||||
a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel'))
|
||||
span.glyphicon.glyphicon-remove(ng-hide='!task._editing')
|
||||
|
|
||||
|
||||
// save
|
||||
a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save'))
|
||||
span.glyphicon.glyphicon-ok(ng-hide='!task._editing')
|
||||
|
|
||||
|
||||
//challenges
|
||||
span(ng-if='task.challenge.id')
|
||||
span(ng-if='task.challenge.broken')
|
||||
@@ -44,8 +54,9 @@
|
||||
span(ng-if='!task.challenge.broken')
|
||||
span.glyphicon.glyphicon-bullhorn(tooltip=env.t('challenge'))
|
||||
|
|
||||
|
||||
// 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
|
||||
|
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@ li(id='task-{{::task._id}}',
|
||||
ng-click='spell && (list.type != "reward") && castEnd(task, "task", $event)',
|
||||
ng-show='shouldShow(task, list, user.preferences)',
|
||||
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')
|
||||
include ./meta_controls
|
||||
|
||||
include ./task_view/index
|
||||
|
||||
include ./edit/index
|
||||
|
||||
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)
|
||||
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')
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
span.reward-cost {{task.value}}
|
||||
|
||||
// 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',
|
||||
ng-model='task.completed', ng-if='$state.includes("tasks")',
|
||||
ng-change='changeCheck(task)'
|
||||
|
||||
Reference in New Issue
Block a user