diff --git a/package.json b/package.json
index cdc9626080..61028f2883 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
index 1c76957d11..a6dafe6541 100644
--- a/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
+++ b/test/api/v3/integration/tasks/groups/DELETE-group_tasks_id.test.js
@@ -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'),
+ });
+ });
});
diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
index d5d81cd4c7..bdeb33be31 100644
--- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_approve_userId.test.js
@@ -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);
diff --git a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
index 85eee161fd..69bc916a0a 100644
--- a/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
+++ b/test/api/v3/integration/tasks/groups/POST-tasks_group_id_assign_user_id.test.js
@@ -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}`);
diff --git a/test/api/v3/unit/libs/payments.test.js b/test/api/v3/unit/libs/payments.test.js
index 43f9db0885..7d28ce5ac8 100644
--- a/test/api/v3/unit/libs/payments.test.js
+++ b/test/api/v3/unit/libs/payments.test.js
@@ -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;
+ });
+ });
});
diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js
index ecc69dff83..124ac9300a 100644
--- a/test/api/v3/unit/models/group.test.js
+++ b/test/api/v3/unit/models/group.test.js
@@ -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';
diff --git a/test/api/v3/unit/models/group_tasks.test.js b/test/api/v3/unit/models/group_tasks.test.js
index c0d6741fa2..331b91ac1b 100644
--- a/test/api/v3/unit/models/group_tasks.test.js
+++ b/test/api/v3/unit/models/group_tasks.test.js
@@ -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 () => {
diff --git a/test/client-old/spec/controllers/challengesCtrlSpec.js b/test/client-old/spec/controllers/challengesCtrlSpec.js
index b0e12f5fd6..c360accaff 100644
--- a/test/client-old/spec/controllers/challengesCtrlSpec.js
+++ b/test/client-old/spec/controllers/challengesCtrlSpec.js
@@ -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);
});
});
});
diff --git a/test/client-old/spec/controllers/tasksCtrlSpec.js b/test/client-old/spec/controllers/tasksCtrlSpec.js
index 7d02a6edbb..318b7aef81 100644
--- a/test/client-old/spec/controllers/tasksCtrlSpec.js
+++ b/test/client-old/spec/controllers/tasksCtrlSpec.js
@@ -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);
});
});
});
diff --git a/test/client-old/spec/services/taskServicesSpec.js b/test/client-old/spec/services/taskServicesSpec.js
index 6dfe84f948..3c51ab1da9 100644
--- a/test/client-old/spec/services/taskServicesSpec.js
+++ b/test/client-old/spec/services/taskServicesSpec.js
@@ -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(){
diff --git a/test/common/ops/scoreTask.test.js b/test/common/ops/scoreTask.test.js
index 572c261d96..45f2a81eaf 100644
--- a/test/common/ops/scoreTask.test.js
+++ b/test/common/ops/scoreTask.test.js
@@ -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 };
diff --git a/test/helpers/globals.helper.js b/test/helpers/globals.helper.js
index 23c9a24e3c..02966ef743 100644
--- a/test/helpers/globals.helper.js
+++ b/test/helpers/globals.helper.js
@@ -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;
diff --git a/website/client-old/css/tasks.styl b/website/client-old/css/tasks.styl
index bcfafab24f..ad101c9ea5 100644
--- a/website/client-old/css/tasks.styl
+++ b/website/client-old/css/tasks.styl
@@ -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
// -----------------------
diff --git a/website/client-old/js/app.js b/website/client-old/js/app.js
index 2fb1852b97..17e571b26b 100644
--- a/website/client-old/js/app.js
+++ b/website/client-old/js/app.js
@@ -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;
+ });
}]
})
diff --git a/website/client-old/js/components/groupApprovals/groupApprovalsController.js b/website/client-old/js/components/groupApprovals/groupApprovalsController.js
index 628408227f..d463c15db7 100644
--- a/website/client-old/js/components/groupApprovals/groupApprovalsController.js
+++ b/website/client-old/js/components/groupApprovals/groupApprovalsController.js
@@ -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;
+ });
+ };
}]);
diff --git a/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js b/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js
index d8c28b38d7..cb45e9b9f7 100644
--- a/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js
+++ b/website/client-old/js/components/groupMembersAutocomplete/groupMembersAutocompleteDirective.js
@@ -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}));
},
diff --git a/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js b/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js
index 5e6427ca1a..fc4a5d7276 100644
--- a/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js
+++ b/website/client-old/js/components/groupTaskActions/groupTaskActionsController.js
@@ -2,7 +2,7 @@ habitrpg.controller('GroupTaskActionsCtrl', ['$scope', 'Shared', 'Tasks', 'User'
function ($scope, Shared, Tasks, User) {
$scope.assignedMembers = [];
$scope.user = User.user;
-
+
$scope.task._edit.requiresApproval = false;
if ($scope.task.group.approval.required) {
$scope.task._edit.requiresApproval = $scope.task.group.approval.required;
diff --git a/website/client-old/js/components/groupTasks/groupTasksController.js b/website/client-old/js/components/groupTasks/groupTasksController.js
index 5314c705c7..a1ba51b7a8 100644
--- a/website/client-old/js/components/groupTasks/groupTasksController.js
+++ b/website/client-old/js/components/groupTasks/groupTasksController.js
@@ -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;
+ };
}]);
diff --git a/website/client-old/js/controllers/challengesCtrl.js b/website/client-old/js/controllers/challengesCtrl.js
index a465d203e8..b7d3896bca 100644
--- a/website/client-old/js/controllers/challengesCtrl.js
+++ b/website/client-old/js/controllers/challengesCtrl.js
@@ -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
diff --git a/website/client-old/js/controllers/filtersCtrl.js b/website/client-old/js/controllers/filtersCtrl.js
index 0020a17eee..77674e48b5 100644
--- a/website/client-old/js/controllers/filtersCtrl.js
+++ b/website/client-old/js/controllers/filtersCtrl.js
@@ -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;
+ };
}]);
diff --git a/website/client-old/js/controllers/groupPlansCtrl.js b/website/client-old/js/controllers/groupPlansCtrl.js
new file mode 100644
index 0000000000..0ebf814aef
--- /dev/null
+++ b/website/client-old/js/controllers/groupPlansCtrl.js
@@ -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});
+ };
+ }]);
diff --git a/website/client-old/js/controllers/groupsCtrl.js b/website/client-old/js/controllers/groupsCtrl.js
index cafbe3b158..d9963fad71 100644
--- a/website/client-old/js/controllers/groupsCtrl.js
+++ b/website/client-old/js/controllers/groupsCtrl.js
@@ -1,7 +1,7 @@
"use strict";
habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '$http', '$q', 'User', 'Members', '$state', 'Notification',
- function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
+ function($scope, $rootScope, Shared, Groups, $http, $q, User, Members, $state, Notification) {
$scope.isMemberOfPendingQuest = function (userid, group) {
if (!group.quest || !group.quest.members) return false;
if (group.quest.active) return false; // quest is started, not pending
diff --git a/website/client-old/js/controllers/guildsCtrl.js b/website/client-old/js/controllers/guildsCtrl.js
index f2cb14520a..3cba1aa6e1 100644
--- a/website/client-old/js/controllers/guildsCtrl.js
+++ b/website/client-old/js/controllers/guildsCtrl.js
@@ -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');
});
}
}
diff --git a/website/client-old/js/controllers/menuCtrl.js b/website/client-old/js/controllers/menuCtrl.js
index 1dd73d522f..dc5c56895d 100644
--- a/website/client-old/js/controllers/menuCtrl.js
+++ b/website/client-old/js/controllers/menuCtrl.js
@@ -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)) {
diff --git a/website/client-old/js/controllers/notificationCtrl.js b/website/client-old/js/controllers/notificationCtrl.js
index acb91cc510..9f1679150f 100644
--- a/website/client-old/js/controllers/notificationCtrl.js
+++ b/website/client-old/js/controllers/notificationCtrl.js
@@ -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
}
diff --git a/website/client-old/js/controllers/partyCtrl.js b/website/client-old/js/controllers/partyCtrl.js
index 696e1fd5a6..cba94dd059 100644
--- a/website/client-old/js/controllers/partyCtrl.js
+++ b/website/client-old/js/controllers/partyCtrl.js
@@ -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;
diff --git a/website/client-old/js/controllers/tasksCtrl.js b/website/client-old/js/controllers/tasksCtrl.js
index b10d6e3949..26d57b7413 100644
--- a/website/client-old/js/controllers/tasksCtrl.js
+++ b/website/client-old/js/controllers/tasksCtrl.js
@@ -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;
+ };
}]);
diff --git a/website/client-old/js/services/paymentServices.js b/website/client-old/js/services/paymentServices.js
index ed22109511..e64ddb1185 100644
--- a/website/client-old/js/services/paymentServices.js
+++ b/website/client-old/js/services/paymentServices.js
@@ -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();
diff --git a/website/client-old/js/services/taskServices.js b/website/client-old/js/services/taskServices.js
index 732b161c17..1a8cdb69fa 100644
--- a/website/client-old/js/services/taskServices.js
+++ b/website/client-old/js/services/taskServices.js
@@ -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();
});
}
diff --git a/website/client-old/js/services/userServices.js b/website/client-old/js/services/userServices.js
index d2768e172c..807bfd0a8e 100644
--- a/website/client-old/js/services/userServices.js
+++ b/website/client-old/js/services/userServices.js
@@ -329,7 +329,7 @@ angular.module('habitrpg')
},
readNotifications: function (notificationIds) {
- UserNotifications.readNotifications(notificationIds);
+ return UserNotifications.readNotifications(notificationIds);
},
addTag: function(data) {
diff --git a/website/client-old/manifest.json b/website/client-old/manifest.json
index d2ce5704cb..537c83074f 100644
--- a/website/client-old/manifest.json
+++ b/website/client-old/manifest.json
@@ -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",
diff --git a/website/common/locales/en/generic.json b/website/common/locales/en/generic.json
index ffa2270a31..d94354ace5 100644
--- a/website/common/locales/en/generic.json
+++ b/website/common/locales/en/generic.json
@@ -4,7 +4,7 @@
"titleIndex": "Habitica | Your Life The Role Playing Game",
"habitica": "Habitica",
"habiticaLink": "Habitica",
-
+
"titleTasks": "Tasks",
"titleAvatar": "Avatar",
"titleBackgrounds": "Backgrounds",
@@ -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"
}
diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json
index 962722a8e9..c8a2304f45 100644
--- a/website/common/locales/en/groups.json
+++ b/website/common/locales/en/groups.json
@@ -1,224 +1,257 @@
{
- "tavern": "Tavern Chat",
- "innCheckOut": "Check Out of Inn",
- "innCheckIn": "Rest in the Inn",
- "innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
- "innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
- "lfgPosts": "Looking for Group (Party Wanted) Posts",
- "tutorial": "Tutorial",
- "glossary": "Glossary",
- "wiki": "Wiki",
- "wikiLink": "Wiki",
- "reportAP": "Report a Problem",
- "requestAF": "Request a Feature",
- "community": "Community Forum",
- "dataTool": "Data Display Tool",
- "resources": "Resources",
- "askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)",
- "tavernAlert1": "To report a bug, visit",
- "tavernAlert2": "the Report a Bug Guild",
- "moderatorIntro1": "Tavern and guild moderators are: ",
- "communityGuidelines": "Community Guidelines",
- "communityGuidelinesRead1": "Please read our",
- "communityGuidelinesRead2": "before chatting.",
- "party": "Party",
- "createAParty": "Create A Party",
- "updatedParty": "Party settings updated.",
- "noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
- "LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
- "wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
- "joinExistingParty": "Join Someone Else's Party",
- "needPartyToStartQuest": "Whoops! You need to create or join a party before you can start a quest!",
- "create": "Create",
- "userId": "User ID",
- "invite": "Invite",
- "leave": "Leave",
- "invitedTo": "Invited to <%= name %>",
- "invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
- "invitationAcceptedHeader": "Your Invitation has been Accepted",
- "invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
- "joinNewParty": "Join New Party",
- "declineInvitation": "Decline Invitation",
- "partyLoading1": "Your party is being summoned. Please wait...",
- "partyLoading2": "Your party is coming in from battle. Please wait...",
- "partyLoading3": "Your party is gathering. Please wait...",
- "partyLoading4": "Your party is materializing. Please wait...",
- "systemMessage": "System Message",
- "newMsg": "New message in \"<%= name %>\"",
- "chat": "Chat",
- "sendChat": "Send Chat",
- "toolTipMsg": "Fetch Recent Messages",
- "sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
- "syncPartyAndChat": "Sync Party and Chat",
- "guildBankPop1": "Guild Bank",
- "guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
- "guildGems": "Guild Gems",
- "editGroup": "Edit Group",
- "newGroupName": "<%= groupType %> Name",
- "groupName": "Group Name",
- "groupLeader": "Group Leader",
- "groupID": "Group ID",
- "groupDescr": "Description shown in public Guilds list (Markdown OK)",
- "logoUrl": "Logo URL",
- "assignLeader": "Assign Group Leader",
- "members": "Members",
- "partyList": "Order for party members in header",
- "banTip": "Boot Member",
- "moreMembers": "more members",
- "invited": "Invited",
- "leaderMsg": "Message from group leader (Markdown OK)",
- "name": "Name",
- "description": "Description",
- "public": "Public",
- "inviteOnly": "Invite Only",
- "gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
- "search": "Search",
- "publicGuilds": "Public Guilds",
- "createGuild": "Create Guild",
- "guild": "Guild",
- "guilds": "Guilds",
- "guildsLink": "Guilds",
- "sureKick": "Do you really want to remove this member from the party/guild?",
- "optionalMessage": "Optional message",
- "yesRemove": "Yes, remove them",
- "foreverAlone": "Can't like your own message. Don't be that person.",
- "sortLevel": "Sort by level",
- "sortRandom": "Sort randomly",
- "sortPets": "Sort by number of pets",
- "sortName": "Sort by avatar name",
- "sortBackgrounds": "Sort by background",
- "sortHabitrpgJoined": "Sort by Habitica date joined",
- "sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
- "ascendingSort": "Sort Ascending",
- "descendingSort": "Sort Descending",
- "confirmGuild": "Create Guild for 4 Gems?",
- "leaveGroupCha": "Leave Guild challenges and...",
- "confirm": "Confirm",
- "leaveGroup": "Leave Guild?",
- "leavePartyCha": "Leave party challenges and...",
- "leaveParty": "Leave party?",
- "sendPM": "Send private message",
- "send": "Send",
- "messageSentAlert": "Message sent",
- "pmHeading": "Private message to <%= name %>",
- "pmsMarkedRead": "Your private messages have been marked as read",
- "clearAll": "Delete All Messages",
- "confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
- "optOutPopover": "Don't like private messages? Click to completely opt out",
- "block": "Block",
- "unblock": "Un-block",
- "pm-reply": "Send a reply",
- "inbox": "Inbox",
- "messageRequired": "A message is required.",
- "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!",
- "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",
- "abuseFlagModalHeading": "Report <%= name %> for violation?",
- "abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:
- swearing, religous oaths
- bigotry, slurs
- adult topics
- violence, including as a joke
- spam, nonsensical messages
",
- "abuseFlagModalButton": "Report Violation",
- "abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
- "abuseAlreadyReported": "You have already reported this message.",
- "needsText": "Please type a message.",
- "needsTextPlaceholder": "Type your message here.",
- "copyMessageAsToDo": "Copy message as To-Do",
- "messageAddedAsToDo": "Message copied as To-Do.",
- "messageWroteIn": "<%= user %> wrote in <%= group %>",
- "taskFromInbox": "<%= from %> wrote '<%= message %>'",
- "taskTextFromInbox": "Message from <%= from %>",
- "msgPreviewHeading": "Message Preview",
- "leaderOnlyChallenges": "Only group leader can create challenges",
- "sendGift": "Send Gift",
- "inviteFriends": "Invite Friends",
- "inviteByEmail": "Invite by Email",
- "inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
- "inviteFriendsNow": "Invite Friends Now",
- "inviteFriendsLater": "Invite Friends Later",
- "inviteAlertInfo": "If you have friends already using Habitica, invite them by User ID here.",
- "inviteExistUser": "Invite Existing Users",
- "byColon": "By:",
- "inviteNewUsers": "Invite New Users",
- "sendInvitations": "Send Invitations",
- "invitationsSent": "Invitations sent!",
- "invitationSent": "Invitation sent!",
- "inviteAlertInfo2": "Or share this link (copy/paste):",
- "sendGiftHeading": "Send Gift to <%= name %>",
- "sendGiftGemsBalance": "From <%= number %> Gems",
- "sendGiftCost": "Total: $<%= cost %> USD",
- "sendGiftFromBalance": "From Balance",
- "sendGiftPurchase": "Purchase",
- "sendGiftMessagePlaceholder": "Personal message (optional)",
- "sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
- "battleWithFriends": "Battle Monsters With Friends",
- "startPartyWithFriends": "Start a Party with your friends!",
- "startAParty": "Start a Party",
- "addToParty": "Add someone to your party",
- "likePost": "Click if you like this post!",
- "partyExplanation1": "Play Habitica with friends to stay accountable!",
- "partyExplanation2": "Battle monsters and create Challenges!",
- "partyExplanation3": "Invite friends now to earn a Quest Scroll!",
- "wantToStartParty": "Do you want to start a party?",
- "exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
- "nameYourParty": "Name your new party!",
- "partyEmpty": "You're the only one in your party. Invite your friends!",
- "partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
- "guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
- "possessiveParty": "<%= name %>'s Party",
- "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!",
- "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.",
- "groupTypesRequired": "You must supply a valid \"type\" query string.",
- "questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
- "cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
- "onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
- "memberCannotRemoveYourself": "You cannot remove yourself!",
- "groupMemberNotFound": "User not found among group's members",
- "mustBeGroupMember": "Must be member of the group.",
- "keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
- "keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
- "canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
- "inviteMissingEmail": "Missing email address in invite.",
- "inviteMissingUuid": "Missing user id in invite",
- "inviteMustNotBeEmpty": "Invite must not be empty.",
- "partyMustbePrivate": "Parties must be private",
- "userAlreadyInGroup": "User already in that group.",
- "cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
- "userAlreadyInvitedToGroup": "User already invited to that group.",
- "userAlreadyPendingInvitation": "User already pending invitation.",
- "userAlreadyInAParty": "User already in a party.",
- "userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
- "userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
- "uuidsMustBeAnArray": "User ID invites must be an array.",
- "emailsMustBeAnArray": "Email address invites must be an array.",
- "canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
- "onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
- "onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
- "onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
- "newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
- "newChatMessageTitle": "New message in <%= groupName %>",
- "exportInbox": "Export Messages",
- "exportInboxPopoverTitle": "Export your messages as HTML",
- "exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
- "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.
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.
This box will close automatically when a decision is made.",
- "confirmAddTag": "Do you really want to add \"<%= tag %>\"?",
- "confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
- "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.
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.
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",
- "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 %>"
-}
+ "tavern": "Tavern Chat",
+ "innCheckOut": "Check Out of Inn",
+ "innCheckIn": "Rest in the Inn",
+ "innText": "You're resting in the Inn! While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day. Be warned: If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies unless they are also in the Inn! Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn.",
+ "innTextBroken": "You're resting in the Inn, I guess... While checked-in, your Dailies won't hurt you at the day's end, but they will still refresh every day... If you are participating in a Boss Quest, the Boss will still damage you for your party mates' missed Dailies... unless they are also in the Inn... Also, your own damage to the Boss (or items collected) will not be applied until you check out of the Inn... so tired...",
+ "lfgPosts": "Looking for Group (Party Wanted) Posts",
+ "tutorial": "Tutorial",
+ "glossary": "Glossary",
+ "wiki": "Wiki",
+ "wikiLink": "Wiki",
+ "reportAP": "Report a Problem",
+ "requestAF": "Request a Feature",
+ "community": "Community Forum",
+ "dataTool": "Data Display Tool",
+ "resources": "Resources",
+ "askQuestionNewbiesGuild": "Ask a Question (Newbies Guild)",
+ "tavernAlert1": "To report a bug, visit",
+ "tavernAlert2": "the Report a Bug Guild",
+ "moderatorIntro1": "Tavern and guild moderators are: ",
+ "communityGuidelines": "Community Guidelines",
+ "communityGuidelinesRead1": "Please read our",
+ "communityGuidelinesRead2": "before chatting.",
+ "party": "Party",
+ "createAParty": "Create A Party",
+ "updatedParty": "Party settings updated.",
+ "noPartyText": "You are either not in a party or your party is taking a while to load. You can either create one and invite friends, or if you want to join an existing party, have them enter your Unique User ID below and then come back here to look for the invitation:",
+ "LFG": "To advertise your new party or find one to join, go to the <%= linkStart %>Party Wanted (Looking for Group)<%= linkEnd %> Guild.",
+ "wantExistingParty": "Want to join an existing party? Go to the <%= linkStart %>Party Wanted Guild<%= linkEnd %> and post this User ID:",
+ "joinExistingParty": "Join Someone Else's Party",
+ "needPartyToStartQuest": "Whoops! You need to create or join a party before you can start a quest!",
+ "create": "Create",
+ "userId": "User ID",
+ "invite": "Invite",
+ "leave": "Leave",
+ "invitedTo": "Invited to <%= name %>",
+ "invitedToNewParty": "You were invited to join a party! Do you want to leave this party and join <%= partyName %>?",
+ "invitationAcceptedHeader": "Your Invitation has been Accepted",
+ "invitationAcceptedBody": "<%= username %> accepted your invitation to <%= groupName %>!",
+ "joinNewParty": "Join New Party",
+ "declineInvitation": "Decline Invitation",
+ "partyLoading1": "Your party is being summoned. Please wait...",
+ "partyLoading2": "Your party is coming in from battle. Please wait...",
+ "partyLoading3": "Your party is gathering. Please wait...",
+ "partyLoading4": "Your party is materializing. Please wait...",
+ "systemMessage": "System Message",
+ "newMsg": "New message in \"<%= name %>\"",
+ "chat": "Chat",
+ "sendChat": "Send Chat",
+ "toolTipMsg": "Fetch Recent Messages",
+ "sendChatToolTip": "You can send a chat from the keyboard by tabbing to the 'Send Chat' button and pressing Enter or by pressing Control (Command on a Mac) + Enter.",
+ "syncPartyAndChat": "Sync Party and Chat",
+ "guildBankPop1": "Guild Bank",
+ "guildBankPop2": "Gems which your guild leader can use for challenge prizes.",
+ "guildGems": "Guild Gems",
+ "editGroup": "Edit Group",
+ "newGroupName": "<%= groupType %> Name",
+ "groupName": "Group Name",
+ "groupLeader": "Group Leader",
+ "groupID": "Group ID",
+ "groupDescr": "Description shown in public Guilds list (Markdown OK)",
+ "logoUrl": "Logo URL",
+ "assignLeader": "Assign Group Leader",
+ "members": "Members",
+ "partyList": "Order for party members in header",
+ "banTip": "Boot Member",
+ "moreMembers": "more members",
+ "invited": "Invited",
+ "leaderMsg": "Message from group leader (Markdown OK)",
+ "name": "Name",
+ "description": "Description",
+ "public": "Public",
+ "inviteOnly": "Invite Only",
+ "gemCost": "The Gem cost promotes high quality Guilds, and is transferred into your Guild's bank for use as prizes in Guild Challenges!",
+ "search": "Search",
+ "publicGuilds": "Public Guilds",
+ "createGuild": "Create Guild",
+ "guild": "Guild",
+ "guilds": "Guilds",
+ "guildsLink": "Guilds",
+ "sureKick": "Do you really want to remove this member from the party/guild?",
+ "optionalMessage": "Optional message",
+ "yesRemove": "Yes, remove them",
+ "foreverAlone": "Can't like your own message. Don't be that person.",
+ "sortLevel": "Sort by level",
+ "sortRandom": "Sort randomly",
+ "sortPets": "Sort by number of pets",
+ "sortName": "Sort by avatar name",
+ "sortBackgrounds": "Sort by background",
+ "sortHabitrpgJoined": "Sort by Habitica date joined",
+ "sortHabitrpgLastLoggedIn": "Sort by last time user logged in",
+ "ascendingSort": "Sort Ascending",
+ "descendingSort": "Sort Descending",
+ "confirmGuild": "Create Guild for 4 Gems?",
+ "leaveGroupCha": "Leave Guild challenges and...",
+ "confirm": "Confirm",
+ "leaveGroup": "Leave Guild?",
+ "leavePartyCha": "Leave party challenges and...",
+ "leaveParty": "Leave party?",
+ "sendPM": "Send private message",
+ "send": "Send",
+ "messageSentAlert": "Message sent",
+ "pmHeading": "Private message to <%= name %>",
+ "pmsMarkedRead": "Your private messages have been marked as read",
+ "clearAll": "Delete All Messages",
+ "confirmDeleteAllMessages": "Are you sure you want to delete all messages in your inbox? Other users will still see messages you have sent to them.",
+ "optOutPopover": "Don't like private messages? Click to completely opt out",
+ "block": "Block",
+ "unblock": "Un-block",
+ "pm-reply": "Send a reply",
+ "inbox": "Inbox",
+ "messageRequired": "A message is required.",
+ "toUserIDRequired": "A User ID is required",
+ "gemAmountRequired": "A number of gems is required",
+ "notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
+ "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",
+ "abuseFlagModalHeading": "Report <%= name %> for violation?",
+ "abuseFlagModalBody": "Are you sure you want to report this post? You should ONLY report a post that violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Inappropriately reporting a post is a violation of the Community Guidelines and may give you an infraction. Appropriate reasons to flag a post include but are not limited to:
- swearing, religous oaths
- bigotry, slurs
- adult topics
- violence, including as a joke
- spam, nonsensical messages
",
+ "abuseFlagModalButton": "Report Violation",
+ "abuseReported": "Thank you for reporting this violation. The moderators have been notified.",
+ "abuseAlreadyReported": "You have already reported this message.",
+ "needsText": "Please type a message.",
+ "needsTextPlaceholder": "Type your message here.",
+ "copyMessageAsToDo": "Copy message as To-Do",
+ "messageAddedAsToDo": "Message copied as To-Do.",
+ "messageWroteIn": "<%= user %> wrote in <%= group %>",
+ "taskFromInbox": "<%= from %> wrote '<%= message %>'",
+ "taskTextFromInbox": "Message from <%= from %>",
+ "msgPreviewHeading": "Message Preview",
+ "leaderOnlyChallenges": "Only group leader can create challenges",
+ "sendGift": "Send Gift",
+ "inviteFriends": "Invite Friends",
+ "inviteByEmail": "Invite by Email",
+ "inviteByEmailExplanation": "If a friend joins Habitica via your email, they'll automatically be invited to your party!",
+ "inviteFriendsNow": "Invite Friends Now",
+ "inviteFriendsLater": "Invite Friends Later",
+ "inviteAlertInfo": "If you have friends already using Habitica, invite them by User ID here.",
+ "inviteExistUser": "Invite Existing Users",
+ "byColon": "By:",
+ "inviteNewUsers": "Invite New Users",
+ "sendInvitations": "Send Invitations",
+ "invitationsSent": "Invitations sent!",
+ "invitationSent": "Invitation sent!",
+ "inviteAlertInfo2": "Or share this link (copy/paste):",
+ "sendGiftHeading": "Send Gift to <%= name %>",
+ "sendGiftGemsBalance": "From <%= number %> Gems",
+ "sendGiftCost": "Total: $<%= cost %> USD",
+ "sendGiftFromBalance": "From Balance",
+ "sendGiftPurchase": "Purchase",
+ "sendGiftMessagePlaceholder": "Personal message (optional)",
+ "sendGiftSubscription": "<%= months %> Month(s): $<%= price %> USD",
+ "battleWithFriends": "Battle Monsters With Friends",
+ "startPartyWithFriends": "Start a Party with your friends!",
+ "startAParty": "Start a Party",
+ "addToParty": "Add someone to your party",
+ "likePost": "Click if you like this post!",
+ "partyExplanation1": "Play Habitica with friends to stay accountable!",
+ "partyExplanation2": "Battle monsters and create Challenges!",
+ "partyExplanation3": "Invite friends now to earn a Quest Scroll!",
+ "wantToStartParty": "Do you want to start a party?",
+ "exclusiveQuestScroll": "Inviting a friend to your party will grant you an exclusive Quest Scroll to battle the Basi-List together!",
+ "nameYourParty": "Name your new party!",
+ "partyEmpty": "You're the only one in your party. Invite your friends!",
+ "partyChatEmpty": "Your party chat is empty! Type a message in the box above to start chatting.",
+ "guildChatEmpty": "This guild's chat is empty! Type a message in the box above to start chatting.",
+ "possessiveParty": "<%= name %>'s Party",
+ "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",
+ "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.",
+ "groupTypesRequired": "You must supply a valid \"type\" query string.",
+ "questLeaderCannotLeaveGroup": "You cannot leave your party when you have started a quest. Abort the quest first.",
+ "cannotLeaveWhileActiveQuest": "You cannot leave party during an active quest. Please leave the quest first.",
+ "onlyLeaderCanRemoveMember": "Only group leader can remove a member!",
+ "memberCannotRemoveYourself": "You cannot remove yourself!",
+ "groupMemberNotFound": "User not found among group's members",
+ "mustBeGroupMember": "Must be member of the group.",
+ "keepOrRemoveAll": "req.query.keep must be either \"keep-all\" or \"remove-all\"",
+ "keepOrRemove": "req.query.keep must be either \"keep\" or \"remove\"",
+ "canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
+ "inviteMissingEmail": "Missing email address in invite.",
+ "inviteMissingUuid": "Missing user id in invite",
+ "inviteMustNotBeEmpty": "Invite must not be empty.",
+ "partyMustbePrivate": "Parties must be private",
+ "userAlreadyInGroup": "User already in that group.",
+ "cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
+ "userAlreadyInvitedToGroup": "User already invited to that group.",
+ "userAlreadyPendingInvitation": "User already pending invitation.",
+ "userAlreadyInAParty": "User already in a party.",
+ "userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
+ "userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
+ "uuidsMustBeAnArray": "User ID invites must be an array.",
+ "emailsMustBeAnArray": "Email address invites must be an array.",
+ "canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
+ "onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
+ "onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
+ "onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
+ "newChatMessagePlainNotification": "New message in <%= groupName %> by <%= authorName %>. Click here to open the chat page!",
+ "newChatMessageTitle": "New message in <%= groupName %>",
+ "exportInbox": "Export Messages",
+ "exportInboxPopoverTitle": "Export your messages as HTML",
+ "exportInboxPopoverBody": "HTML allows easy reading of messages in a browser. For a machine-readable format, use Data > Export Data",
+ "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.
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.
This box will close automatically when a decision is made.",
+ "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.
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.
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 \"<%= taskText %>\" has been approved",
+ "userHasRequestedTaskApproval": "<%= user %> has requested task approval for <%= taskName %>",
+ "approve": "Approve",
+ "approvalTitle": "<%= text %> for user: <%= userName %>",
+ "confirmTaskApproval": "Do you want to reward <%= username %> for completing this task?",
+ "groupSubscriptionPrice": "$9 every month + $3 a month for every additional group member",
+
+ "groupBenefitsTitle": "How a group plan can help you",
+ "groupBenefitsDescription": "We’ve just launched the beta version of our group plans! Upgrading to a group plan unlocks some unique features to optimize the social side of Habitica.",
+ "groupBenefitOneTitle": "Create a shared task list",
+ "groupBenefitOneDescription": "Set up a shared task list for the group that everyone can easily view and edit.",
+ "groupBenefitTwoTitle": "Assign tasks to group members",
+ "groupBenefitTwoDescription": "Want a coworker to answer a critical email? Need your roommate to pick up the groceries? Just assign them the tasks you create, and they’ll automatically appear in that person’s task dashboard.",
+ "groupBenefitThreeTitle": "Claim a task that you are working on",
+ "groupBenefitThreeDescription": "Stake your claim on any group task with a simple click. Make it clear what everybody is working on!",
+ "groupBenefitFourTitle": "Mark tasks that require special approval",
+ "groupBenefitFourDescription": "Need to verify that a task really did get done before that user gets their rewards? Just adjust the approval settings for added control.",
+ "groupBenefitFiveTitle": "Chat privately with your group",
+ "groupBenefitFiveDescription": "Stay in the loop about important decisions in our easy-to-use chatroom!",
+ "createAGroup": "Create A Group",
+
+ "assignFieldPlaceholder": "Type a group member's profile name",
+ "cannotDeleteActiveGroup": "You cannot remove a group with an active subscription",
+ "groupTasksTitle": "Group Tasks List",
+ "approvalsTitle": "Tasks Awaiting Approval",
+ "upgradeTitle": "Upgrade",
+ "blankApprovalsDescription": "When your group completes tasks that need your approval, they’ll appear here! Adjust approval requirement settings under task editing.",
+ "userIsClamingTask": "<%= username %> has claimed \"<%= task %>\"",
+ "approvalRequested": "Approval Requested",
+ "refreshApprovals": "Refresh Approvals",
+ "refreshGroupTasks": "Refresh Group Tasks",
+ "claimedBy": "\n\nClaimed by: <%= claimingUsers %>",
+ "cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
+ "confirmGuildPlanCreation": "Create this group?"
+}
diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js
index 1868a2e96c..54e030fff6 100644
--- a/website/common/script/content/index.js
+++ b/website/common/script/content/index.js
@@ -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: [
{
diff --git a/website/common/script/content/subscriptionBlocks.js b/website/common/script/content/subscriptionBlocks.js
new file mode 100644
index 0000000000..d67cfc2195
--- /dev/null
+++ b/website/common/script/content/subscriptionBlocks.js
@@ -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;
diff --git a/website/common/script/ops/scoreTask.js b/website/common/script/ops/scoreTask.js
index b0a973314c..cba7818c32 100644
--- a/website/common/script/ops/scoreTask.js
+++ b/website/common/script/ops/scoreTask.js
@@ -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 = {};
diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js
index 8a066682b9..906b40c596 100644
--- a/website/server/controllers/api-v3/groups.js
+++ b/website/server/controllers/api-v3/groups.js
@@ -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;
diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js
index 4b4c62adbf..aaa626b772 100644
--- a/website/server/controllers/api-v3/tasks.js
+++ b/website/server/controllers/api-v3/tasks.js
@@ -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) {
diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js
index adca492d01..b1f5af3628 100644
--- a/website/server/controllers/api-v3/tasks/groups.js
+++ b/website/server/controllers/api-v3/tasks/groups.js
@@ -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();
diff --git a/website/server/controllers/top-level/payments/amazon.js b/website/server/controllers/top-level/payments/amazon.js
index 9fb194fa37..0a74b37084 100644
--- a/website/server/controllers/top-level/payments/amazon.js
+++ b/website/server/controllers/top-level/payments/amazon.js
@@ -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) {
diff --git a/website/server/controllers/top-level/payments/stripe.js b/website/server/controllers/top-level/payments/stripe.js
index 9b433f4130..c0ba36cd60 100644
--- a/website/server/controllers/top-level/payments/stripe.js
+++ b/website/server/controllers/top-level/payments/stripe.js
@@ -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;
diff --git a/website/server/libs/payments.js b/website/server/libs/payments.js
index c9193aeabb..aa17fe7c0c 100644
--- a/website/server/libs/payments.js
+++ b/website/server/libs/payments.js
@@ -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;
diff --git a/website/server/middlewares/cron.js b/website/server/middlewares/cron.js
index 495a2e9514..18bed327e7 100644
--- a/website/server/middlewares/cron.js
+++ b/website/server/middlewares/cron.js
@@ -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
diff --git a/website/server/models/group.js b/website/server/models/group.js
index ea99e52e9b..1a6b652606 100644
--- a/website/server/models/group.js
+++ b/website/server/models/group.js
@@ -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
diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js
index 997560112e..38c28057aa 100644
--- a/website/server/models/subscriptionPlan.js
+++ b/website/server/models/subscriptionPlan.js
@@ -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,
diff --git a/website/server/models/tag.js b/website/server/models/tag.js
index dd5d14243e..d8ddde1f44 100644
--- a/website/server/models/tag.js
+++ b/website/server/models/tag.js
@@ -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
diff --git a/website/server/models/task.js b/website/server/models/task.js
index 8de85da31e..83d17eb009 100644
--- a/website/server/models/task.js
+++ b/website/server/models/task.js
@@ -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},
}],
};
};
diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js
index f7b2c9f81b..1fec849c35 100644
--- a/website/server/models/userNotification.js
+++ b/website/server/models/userNotification.js
@@ -16,6 +16,7 @@ const NOTIFICATION_TYPES = [
'GROUP_TASK_APPROVED',
'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED',
+ 'SCORED_TASK',
];
const Schema = mongoose.Schema;
diff --git a/website/views/main/filters.jade b/website/views/main/filters.jade
index 40e11817a8..c19425e3aa 100644
--- a/website/views/main/filters.jade
+++ b/website/views/main/filters.jade
@@ -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}}
//
diff --git a/website/views/options/settings.jade b/website/views/options/settings.jade
index 623718a184..77463de587 100644
--- a/website/views/options/settings.jade
+++ b/website/views/options/settings.jade
@@ -366,65 +366,5 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
span=env.t('unsubscribeAllEmails')
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')} {{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
- 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
diff --git a/website/views/options/settings/subscription.jade b/website/views/options/settings/subscription.jade
new file mode 100644
index 0000000000..e8903da0c1
--- /dev/null
+++ b/website/views/options/settings/subscription.jade
@@ -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')} {{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
+ 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'))
diff --git a/website/views/options/social/create-group.jade b/website/views/options/social/create-group.jade
deleted file mode 100644
index 846bd99837..0000000000
--- a/website/views/options/social/create-group.jade
+++ /dev/null
@@ -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'))
diff --git a/website/views/options/social/group.jade b/website/views/options/social/group.jade
index 03c582fe70..1bdf77128c 100644
--- a/website/views/options/social/group.jade
+++ b/website/views/options/social/group.jade
@@ -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.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
-
- // ------ 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
- select.form-control#partyOrder(
- ng-model='user.party.order',
- ng-controller='ChatCtrl',
- ng-options='k as v for (k , v) in partyOrderChoices',
- ng-change='set({"party.order": user.party.order})'
- )
- |
- select.form-control#partyOrderAscending(
- ng-model='user.party.orderAscending',
- ng-controller='ChatCtrl',
- ng-options='k as v for (k , v) in partyOrderAscendingChoices',
- ng-change='set({"party.orderAscending": user.party.orderAscending})'
- )
-
- include ./challenge-box
-
- div(ng-if="group")
- a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
-
- .slight-vertical-padding
- small.muted=env.t('groupID')
- | : {{group._id}}
- .slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
- small.muted=env.t('userId')
- | : {{user._id}}
-
- .col-md-8
- div
- textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
- .slight-vertical-padding
- table(ng-show='group.leaderMessage')
- tr
- td
- .popover.static-popover.fade.right.in.wide-popover
- .arrow
- h3.popover-title {{group.leader.profile.name}}
- .popover-content
- markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
-
- ul.options-menu(ng-init="groupPane = 'chat'", ng-hide="true")
- //- li
- //- a(ng-click="groupPane = 'chat'")=env.t('chat')
- //- li
- //- a(ng-click="groupPane = 'tasks'", ng-show='group.purchased.active')=env.t('tasks')
- //- li
- //- a(ng-click="groupPane = 'approvals'", ng-show='group.purchased.active && group.leader._id === user._id')=env.t('approvals')
- //- li
- //- a(ng-click="groupPane = 'subscription'", ng-show='group.leader._id === user._id')=env.t('subscription')
+ 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')
- .tab-content
- .tab-pane.active
+ .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
+ .row(ng-show="groupPanel == 'chat'")
+ .col-md-4
- +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')} {{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
- 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}}
+ // ------ Bosses -------
+ +boss(false, false)
- 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')
+ // ------ 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')
- .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'))
+ .panel-body
+ form(ng-show='group._editing')
+ .form-group
+ label=env.t('groupName')
+ input.form-control(type='text', ng-model='groupCopy.name', placeholder=env.t('groupName'))
+ .form-group
+ label=env.t('description')
+ textarea.form-control(rows=6, ng-model='groupCopy.description')
+ include ../../shared/formatting-help
+ .form-group
+ label=env.t('logoUrl')
+ input.form-control(type='url', placeholder=env.t('logoUrl'), ng-model='groupCopy.logo')
+ .form-group
+ .checkbox
+ label
+ input(type='checkbox', ng-model='groupCopy.leaderOnly.challenges')
+ =env.t('leaderOnlyChallenges')
+
+ h4=env.t('assignLeader')
+ select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
+
+ div(ng-show='!group._editing')
+ img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
+ markdown(text='group.description')
+ br
+ p=env.t('groupLeader')
+ |:
+ a.badge.badge-info(ng-click='clickMember(group.leader._id, true)')
+ | {{group.leader.profile.name}}
+
+ .text-center(ng-if='group.type === "party"')
+ .row.row-margin: .col-sm-6.col-sm-offset-3
+ button.btn.btn-success.btn-block(
+ ng-if='!group.quest.key',
+ ng-click='clickStartQuest();'
+ )=env.t('startAQuest')
+
+ // ------ Members -------
+ .panel.panel-default
+ .panel-heading
+ h3.panel-title
+ =env.t('members')
+ span(ng-if='group.type=="party" && (group.onlineUsers || group.onlineUsers == 0)')= ' (' + env.t('onlineCount', {count: "{{group.onlineUsers}}"}) + ')'
+ button.pull-right.btn.btn-primary(ng-click="openInviteModal(group)")=env.t("inviteFriends")
+
+ .panel-body.modal-fixed-height
+ h4(ng-show='::group.memberCount === 1 && group.type === "party"')=env.t('partyEmpty')
+ table.table.table-striped(ng-show='::group.memberCount > 1 || group.type !== "party"' bindonce='group')
+ tr(ng-repeat='member in group.members track by member._id')
+ td.media
+ // allow leaders to ban members
+ .pull-left(ng-show='group.leader._id == user.id && member._id != user._id')
+ a.media-object(ng-click='removeMember(group, member, true)')
+ span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
+ a.media-body
+ span(ng-click='clickMember(member._id, true)')
+ | {{member.profile.name}}
+ span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
+ | (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
+ .pull-right(ng-if='group.type === "party"')
+ span.text-success {{member.online ? '● ' + env.t('online') : ''}}
+ tr(ng-if='::group.memberCount > group.members.length')
+ td
+ span.badge {{group.memberCount - group.members.length}}
+ = ' ' + env.t('moreMembers')
+ h4(ng-show='group.invites.length > 0')=env.t('invited')
+ table.table.table-striped
+ tr(ng-repeat='invite in group.invites')
+ td.media
+ // allow leaders to ban members
+ .pull-left(ng-show='group.leader._id == user.id')
+ a.media-object(ng-click='removeMember(group, invite, false)')
+ span.glyphicon.glyphicon-ban-circle(tooltip=env.t('banTip'))
+ a.media-body
+ span(ng-click='clickMember(invite._id, true)')
+ | {{invite.profile.name}}
+
+ .panel.panel-default(ng-if='::group.type=="party" && group.memberCount > 1')
+ .panel-heading
+ h3.panel-title=env.t('partyList')
+ .panel-body
+ .form-group
+ select.form-control#partyOrder(
+ ng-model='user.party.order',
+ ng-controller='ChatCtrl',
+ ng-options='k as v for (k , v) in partyOrderChoices',
+ ng-change='set({"party.order": user.party.order})'
+ )
+ |
+ select.form-control#partyOrderAscending(
+ ng-model='user.party.orderAscending',
+ ng-controller='ChatCtrl',
+ ng-options='k as v for (k , v) in partyOrderAscendingChoices',
+ ng-change='set({"party.orderAscending": user.party.orderAscending})'
+ )
+
+ include ./challenge-box
+
+ div(ng-if="group")
+ a.btn.btn-success(ng-if=':: !isMemberOfGroup(User.user._id, group)', ng-click='join(group)')=env.t('join')
+
+ .slight-vertical-padding
+ small.muted=env.t('groupID')
+ | : {{group._id}}
+ .slight-vertical-padding(ng-if='group.type === "party" && group.memberCount === 1')
+ small.muted=env.t('userId')
+ | : {{user._id}}
+
+ .col-md-8
+ div
+ textarea.form-control(ng-show='group._editing', rows=6, placeholder=env.t('leaderMsg'), ng-model='groupCopy.leaderMessage')
+ .slight-vertical-padding
+ table(ng-show='group.leaderMessage')
+ tr
+ td
+ .popover.static-popover.fade.right.in.wide-popover
+ .arrow
+ h3.popover-title {{group.leader.profile.name}}
+ .popover-content
+ markdown(text='group._editing ? groupCopy.leaderMessage : group.leaderMessage')
+
+ div(ng-controller='ChatCtrl')
+ .alert.alert-info.alert-sm(ng-if='group.memberCount > Shared.constants.LARGE_GROUP_COUNT_MESSAGE_CUTOFF')=env.t('largeGroupNote')
+ h3=env.t('chat')
+ include ./chat-box
+
+ +chatMessages()
+ h4(ng-if='group.chat.length < 1 && group.type === "party"')=env.t('partyChatEmpty')
+ h4(ng-if='group.chat.length < 1 && group.type === "guild"')=env.t('guildChatEmpty')
+
+ group-tasks(ng-show="groupPanel == 'tasks'")
+
+ group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
+
+ +groupSubscription
+
diff --git a/website/views/options/social/groups/create-group.jade b/website/views/options/social/groups/create-group.jade
new file mode 100644
index 0000000000..7fdda285d2
--- /dev/null
+++ b/website/views/options/social/groups/create-group.jade
@@ -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'))
diff --git a/website/views/options/social/groups/group-plans-benefits.jade b/website/views/options/social/groups/group-plans-benefits.jade
new file mode 100644
index 0000000000..3f182edf99
--- /dev/null
+++ b/website/views/options/social/groups/group-plans-benefits.jade
@@ -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')
diff --git a/website/views/options/social/groups/group-plans.jade b/website/views/options/social/groups/group-plans.jade
new file mode 100644
index 0000000000..14a7b258cc
--- /dev/null
+++ b/website/views/options/social/groups/group-plans.jade
@@ -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')
diff --git a/website/views/options/social/groups/group-subscription.jade b/website/views/options/social/groups/group-subscription.jade
new file mode 100644
index 0000000000..58c6b2ff05
--- /dev/null
+++ b/website/views/options/social/groups/group-subscription.jade
@@ -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')} {{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}
+ 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')
diff --git a/website/views/options/social/groups/group-tasks-approvals.jade b/website/views/options/social/groups/group-tasks-approvals.jade
index 0d704ec36c..90107b00bd 100644
--- a/website/views/options/social/groups/group-tasks-approvals.jade
+++ b/website/views/options/social/groups/group-tasks-approvals.jade
@@ -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')
diff --git a/website/views/options/social/groups/group-tasks.jade b/website/views/options/social/groups/group-tasks.jade
index 89c8fe8239..b775118d93 100644
--- a/website/views/options/social/groups/group-tasks.jade
+++ b/website/views/options/social/groups/group-tasks.jade
@@ -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)
diff --git a/website/views/options/social/index.jade b/website/views/options/social/index.jade
index fddc0be8a0..18ff56524b 100644
--- a/website/views/options/social/index.jade
+++ b/website/views/options/social/index.jade
@@ -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
diff --git a/website/views/shared/header/menu.jade b/website/views/shared/header/menu.jade
index 8a22563ff6..fcda892473 100644
--- a/website/views/shared/header/menu.jade
+++ b/website/views/shared/header/menu.jade
@@ -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')
diff --git a/website/views/shared/modals/index.jade b/website/views/shared/modals/index.jade
index df9dbd9078..f8a8330c45 100644
--- a/website/views/shared/modals/index.jade
+++ b/website/views/shared/modals/index.jade
@@ -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')
diff --git a/website/views/shared/modals/tasks-edit.jade b/website/views/shared/modals/tasks-edit.jade
new file mode 100644
index 0000000000..1087f856fb
--- /dev/null
+++ b/website/views/shared/modals/tasks-edit.jade
@@ -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
diff --git a/website/views/shared/tasks/edit/index.jade b/website/views/shared/tasks/edit/index.jade
index 1ed0f4f3f8..9ab3ce86bb 100644
--- a/website/views/shared/tasks/edit/index.jade
+++ b/website/views/shared/tasks/edit/index.jade
@@ -1,6 +1,7 @@
div(ng-if='task._editing')
.task-options
-
+ h2 {{task._edit.text}}
+
// Broken Challenge
.well(ng-if='task.challenge.broken')
div(ng-if='task.challenge.broken=="TASK_DELETED" || task.challenge.broken=="CHALLENGE_TASK_NOT_FOUND"')
@@ -33,7 +34,7 @@ div(ng-if='task._editing')
include ./checklist
- 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')
+
diff --git a/website/views/shared/tasks/edit/tags.jade b/website/views/shared/tasks/edit/tags.jade
index 8abcedc355..a4f3a9d068 100644
--- a/website/views/shared/tasks/edit/tags.jade
+++ b/website/views/shared/tasks/edit/tags.jade
@@ -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')
diff --git a/website/views/shared/tasks/index.jade b/website/views/shared/tasks/index.jade
index f50455cdf7..1fbd850534 100644
--- a/website/views/shared/tasks/index.jade
+++ b/website/views/shared/tasks/index.jade
@@ -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
diff --git a/website/views/shared/tasks/meta_controls.jade b/website/views/shared/tasks/meta_controls.jade
index bd41ad4a0b..beddb0bb8c 100644
--- a/website/views/shared/tasks/meta_controls.jade
+++ b/website/views/shared/tasks/meta_controls.jade
@@ -3,6 +3,13 @@
// Due Date
span(ng-if='task.type=="todo" && task.date')
span(ng-class='{"label label-danger":(moment(task.date).isBefore(_today, "days") && !task.completed)}') {{task.date | date:(user.preferences.dateFormat.indexOf('yyyy') == 0 ? user.preferences.dateFormat.substr(5) : user.preferences.dateFormat.substr(0,5))}}
+
+ // Approval requested
+ |
+ span(ng-show='task.group.approval.requested && !task.group.approval.approved')
+ span(tooltip=env.t('approvalRequested'))
+ span=env.t('approvalRequested')
+ |
// 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
|
diff --git a/website/views/shared/tasks/task.jade b/website/views/shared/tasks/task.jade
index ad8ee4ffe5..900e39298d 100644
--- a/website/views/shared/tasks/task.jade
+++ b/website/views/shared/tasks/task.jade
@@ -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]')
diff --git a/website/views/shared/tasks/task_view/add_new.jade b/website/views/shared/tasks/task_view/add_new.jade
index bdd50ac987..f4e2a8d628 100644
--- a/website/views/shared/tasks/task_view/add_new.jade
+++ b/website/views/shared/tasks/task_view/add_new.jade
@@ -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')
diff --git a/website/views/shared/tasks/task_view/index.jade b/website/views/shared/tasks/task_view/index.jade
index 9ce097f4b4..59bf5b92ad 100644
--- a/website/views/shared/tasks/task_view/index.jade
+++ b/website/views/shared/tasks/task_view/index.jade
@@ -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)'