Thehollidayinn/group plans part 2 (#8262)

* Added all ui components back

* Added group ui items back and initial group approval directive

* Added approval list view with approving functionality

* Added notification display for group approvals

* Fixed linting issues

* Removed expectation from beforeEach

* Moved string to locale

* Added per use group plan for stripe

* Added tests for stripe group plan upgrade

* Removed paypal option

* Abstract sub blocks. Hit group sub block from user settings page. Added group subscriptin beneifts display

* Fixed lint issue

* Added pricing and adjusted styles

* Moved text to translations

* Added group email types

* Fixed typo

* Fixed group plan abstraction and other style issues

* Fixed email unit test

* Added type to group plan to filter our group plans

* Removed dev protection from routes

* Removed hard coding and fixed upgrade plan

* Added error when group has subscription and tries to remove

* Fixed payment unit tests

* Added custom string and moved subscription check up in the logic

* Added ability for old leader to delete subscription the created

* Allowed old guild leader to edit their group subscription

* Fixed linting and tests

* Added group sub page to user sub settings

* Added approval and group tasks requests back. Hid user group sub on profile

* Added group tasks sync after adding to allow for editing

* Fixed promise chain when resolving group

* Added approvals to group promise chain

* Ensured compelted group todos are not delted at cron

* Updated copy and other minor styles

* Added group field to tags and recolored group tag.

* Added chat message when task is claimed

* Preventing task scoring when approval is needed

* Added approval requested indicator

* Updated column with for tasks on group page

* Added checklist sync on assign

* Added sync for checklist items

* Added checkilist sync when task is updated

* Added checklist sync remove

* Sanatized group tasks when updated

* Fixed lint issues

* Added instant scoring of approved task

* Added task modal

* Fixed editing of challenge and group tasks

* Added cancel button

* Added add new checklist option to update sync

* Added remove for checklist

* Added checklist update

* Added difference check and sync for checklist if there is a diff

* Fixed task syncing

* Fixed linting issues

* Fixed styles and karma tests

* Fixed minor style issues

* Fixed obj transfer on scope

* Fixed broken tests

* Added new benefits page

* Updated group page styles

* Updated benefits page style

* Added translations

* Prevented sync with empty trask list

* Added task title to edit modal

* Added new group plans page and upgrade redirect

* Added group plans redirect to upgrade

* Fixed party home page being hidden and home button click

* Fixed dynamic changing of task status and grey popup

* Fixed tag editing

* Hid benifites information if group has subscription

* Added quotes to task name

* Fixed issue with assigning multiple users

* Added new group plans ctrl

* Hid menu from public guilds

* Fixed task sync issue

* Updated placeholder for assign field

* Added correct cost to subscribe details

* Hid create, edit, delete task options from non group leaders

* Prevented some front end modifications to group tasks

* Hid tags option from group original task

* Added refresh for approvals and group tasks

* Prepend new group tasks

* Fix last checklist item sync

* Fixed casing issue with tags

* Added claimed by message on hover

* Prevent user from deleting assigned task

* Added single route for group plan sign up and payments

* Abstracted stripe payments and added initial tests

* Abstracted amazon and added initial tests

* Fixed create group message

* Update group id check and return group

* Updated to use the new returned group

* Fixed linting and promise issues

* Fixed broken leave test after merge issue

* Fixed undefined approval error and editing/deleting challenge tasks

* Add pricing to group plans, removed confirmation, and fixed redirect after payment

* Updated group plan cost text
This commit is contained in:
Keith Holliday
2016-12-21 13:45:45 -06:00
committed by GitHub
parent 55a8eef3e1
commit ea24eeb019
70 changed files with 2002 additions and 736 deletions

View File

@@ -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",

View File

@@ -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'),
});
});
});

View File

@@ -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);

View File

@@ -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}`);

View File

@@ -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;
});
});
});

View File

@@ -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';

View File

@@ -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 () => {

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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(){

View File

@@ -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 };

View File

@@ -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;

View File

@@ -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
// -----------------------

View File

@@ -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;
});
}]
})

View File

@@ -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;
});
};
}]);

View File

@@ -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}));
},

View File

@@ -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;

View File

@@ -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;
};
}]);

View File

@@ -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

View File

@@ -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;
};
}]);

View File

@@ -0,0 +1,43 @@
"use strict";
/*
A controller to manage the Group Plans page
*/
angular.module('habitrpg')
.controller("GroupPlansCtrl", ['$scope', '$window', 'Groups', 'Payments',
function($scope, $window, Groups, Payments) {
$scope.PAGES = {
BENEFITS: 'benefits',
CREATE_GROUP: 'create-group',
UPGRADE_GROUP: 'upgrade-group',
};
$scope.activePage = $scope.PAGES.BENEFITS;
$scope.newGroup = {
type: 'guild',
privacy: 'private',
};
$scope.PAYMENTS = {
AMAZON: 'amazon',
STRIPE: 'stripe',
};
$scope.changePage = function (page) {
$scope.activePage = page;
$window.scrollTo(0, 0);
};
$scope.newGroupIsReady = function () {
return $scope.newGroup.name && $scope.newGroup.description;
};
$scope.createGroup = function () {
$scope.changePage($scope.PAGES.UPGRADE_GROUP);
};
$scope.upgradeGroup = function (paymentType) {
var subscriptionKey = 'group_monthly'; // @TODO: Get from content API?
if (paymentType === $scope.PAYMENTS.STRIPE) Payments.showStripe({subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
if (paymentType === $scope.PAYMENTS.AMAZON) Payments.amazonPayments.init({type: 'subscription', subscription: subscriptionKey, coupon: null, groupToCreate: $scope.newGroup});
};
}]);

View File

@@ -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

View File

@@ -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');
});
}
}

View File

@@ -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)) {

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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;
};
}]);

View File

@@ -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();

View File

@@ -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();
});
}

View File

@@ -329,7 +329,7 @@ angular.module('habitrpg')
},
readNotifications: function (notificationIds) {
UserNotifications.readNotifications(notificationIds);
return UserNotifications.readNotifications(notificationIds);
},
addTag: function(data) {

View File

@@ -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",

View File

@@ -4,7 +4,7 @@
"titleIndex": "Habitica | Your Life The Role Playing Game",
"habitica": "Habitica",
"habiticaLink": "<a href='http://habitica.wikia.com/wiki/Habitica' target='_blank'>Habitica</a>",
"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"
}

View File

@@ -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": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
"wiki": "Wiki",
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
"reportAP": "Report a Problem",
"requestAF": "Request a Feature",
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
"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 <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> 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": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
"sureKick": "Do you really want to remove this member from the party/guild?",
"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:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
"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 <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> 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.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"confirmAddTag": "Do you really want to add \"<%= tag %>\"?",
"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.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"claim": "Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task has been approved",
"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": "<a target='_blank' href='http://habitica.wikia.com/wiki/Glossary'>Glossary</a>",
"wiki": "Wiki",
"wikiLink": "<a target='_blank' href='http://habitica.wikia.com/'>Wiki</a>",
"reportAP": "Report a Problem",
"requestAF": "Request a Feature",
"community": "<a target='_blank' href='http://habitica.wikia.com/wiki/Special:Forum'>Community Forum</a>",
"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 <a href='http://habitica.wikia.com/wiki/Party' target='_blank'>create or join a party</a> 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": "<a href='http://habitica.wikia.com/wiki/Guilds' target='_blank'>Guilds</a>",
"sureKick": "Do you really want to remove this member from the party/guild?",
"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:<br><br><ul style='margin-left: 10px;'><li>swearing, religous oaths</li><li>bigotry, slurs</li><li>adult topics</li><li>violence, including as a joke</li><li>spam, nonsensical messages</li></ul>",
"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 <a href='http://habitica.wikia.com/wiki/API_Options' target='_blank'>User ID</a> 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.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"confirmAddTag": "Do you want to assign this task to \"<%= tag %>\"?",
"confirmRemoveTag": "Do you really want to remove \"<%= tag %>\"?",
"groupHomeTitle": "Home",
"assignTask": "Assign Task",
"desktopNotificationsText": "We need your permission to enable desktop notifications for new messages in party chat! Follow your browser's instructions to turn them on.<br><br>You'll receive these notifications only while you have Habitica open. If you decide you don't like them, they can be disabled in your browser's settings.<br><br>This box will close automatically when a decision is made.",
"claim": "Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Your task \"<%= 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": "Weve 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 theyll automatically appear in that persons 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, theyll 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?"
}

View File

@@ -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: [
{

View File

@@ -0,0 +1,39 @@
/* eslint-disable camelcase */
import _ from 'lodash';
let subscriptionBlocks = {
basic_earned: {
months: 1,
price: 5,
},
basic_3mo: {
months: 3,
price: 15,
},
basic_6mo: {
months: 6,
price: 30,
},
google_6mo: {
months: 6,
price: 24,
discount: true,
original: 30,
},
basic_12mo: {
months: 12,
price: 48,
},
group_monthly: {
type: 'group',
months: 1,
price: 9,
quantity: 3, // Default quantity for Stripe - The same as having 3 user subscriptions
},
};
_.each(subscriptionBlocks, function createKeys (b, k) {
return b.key = k;
});
module.exports = subscriptionBlocks;

View File

@@ -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 = {};

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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},
}],
};
};

View File

@@ -16,6 +16,7 @@ const NOTIFICATION_TYPES = [
'GROUP_TASK_APPROVED',
'LOGIN_INCENTIVE',
'GROUP_INVITE_ACCEPTED',
'SCORED_TASK',
];
const Schema = mongoose.Schema;

View File

@@ -34,8 +34,9 @@
button(type='button', ng-click='User.deleteTag({params:{id:tag.id}})')
span.glyphicon.glyphicon-trash
ul(ng-if='!_editing', hrpg-sort-tags)
li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: tag.challenge}', ng-repeat='tag in user.tags', bindonce='user.tags')
li.filters-tags(ng-class='{active: user.filters[tag.id], challenge: showChallengeClass(tag)}', ng-repeat='tag in user.tags', bindonce='user.tags')
a(ng-click='toggleFilter(tag)')
span.glyphicon.glyphicon-bullhorn(ng-if="::tag.challenge")
markdown(text='tag.name')
| {{tag}}
// <li class="{#unless activeFilters(users[_userId].filters)}hidden{/}">

View File

@@ -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')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{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
| &nbsp;#{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

View File

@@ -0,0 +1,63 @@
script(id='partials/options.settings.subscription.html',type='text/ng-template', ng-init="groupPane = 'subscription'")
//-h2=env.t('individualSub')
//- +groupSubscription
.container-fluid(ng-init='_subscription={key:"basic_earned"}')
h3= env.t('benefits')
.row
.col-md-6
+subPerks()
.col-md-6
table.table.alert.alert-info(ng-if='user.purchased.plan.customerId')
tr(ng-if='user.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(user.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!user.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='user.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[user.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[user.purchased.plan.planId].months}}', plan: '{{user.purchased.plan.paymentMethod}}'})
tr(ng-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{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
| &nbsp;#{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'))

View File

@@ -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'))

View File

@@ -1,3 +1,6 @@
include ./groups/group-subscription
include ./groups/group-plans-benefits
a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter', popover-title=env.t('guildBankPop1'), popover=env.t('guildBankPop2'), popover-placement='left')
// <span class="task-action-btn tile flush bright add-gems-btn"></span>
span.task-action-btn.tile.flush.neutral
@@ -6,208 +9,174 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
=' ' + env.t('guildGems')
.container-fluid
.row
.col-md-4
// ------ 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 ? '&#9679 ' + 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})'
)
| &nbsp;
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')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!group.purchased.plan.dateTerminated'): td
h4=env.t('subscribed')
p(ng-if='group.purchased.plan.planId')=env.t('purchasedPlanId', {price: '{{Content.subscriptionBlocks[group.purchased.plan.planId].price}}', months: '{{Content.subscriptionBlocks[group.purchased.plan.planId].months}}', plan: '{{group.purchased.plan.paymentMethod}}'})
tr(ng-if='group.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{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
| &nbsp;#{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 ? '&#9679 ' + 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})'
)
| &nbsp;
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

View File

@@ -0,0 +1,29 @@
mixin groupCreateForm()
form.col-md-12.form-horizontal(ng-submit='create(newGroup)')
.form-group
label.control-label(for='new-group-name')=env.t('newGroupName', {groupType: "{{text}}"})
input.form-control#new-group-name.input-medium.option-content(required, type='text', placeholder=env.t('newGroupName', {groupType: "{{text}}"}), ng-model='newGroup.name')
.form-group
label(for='new-group-description')=env.t('description')
textarea.form-control#new-group-description.option-content(cols='3', placeholder=env.t('description'), ng-model='newGroup.description')
.form-group(ng-show='type=="guild"')
.radio
label
input(type='radio', name='new-group-privacy', value='public', ng-model='newGroup.privacy')
=env.t('public')
.radio
label
input(type='radio', name='new-group-privacy', value='private', ng-model='newGroup.privacy')
=env.t('inviteOnly')
br
input.btn.btn-default(type='submit', ng-disabled='!newGroup.privacy && !newGroup.name', value=env.t('create'))
span.gem-cost= '4 ' + env.t('gems')
p
small=env.t('gemCost')
.form-group
.checkbox
label
input(type='checkbox', ng-model='newGroup.leaderOnly.challenges')
=env.t('leaderOnlyChallenges')
.form-group(ng-show='type=="party"')
input.btn.btn-default.form-control(type='submit', value=env.t('create'))

View File

@@ -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')

View File

@@ -0,0 +1,40 @@
include ./create-group
script(type='text/ng-template', id='partials/options.social.groupPlans.html')
div(ng-show='activePage === PAGES.BENEFITS')
+groupPlansBenefits
br
br
.row
.col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="changePage(PAGES.CREATE_GROUP)")=env.t('createAGroup')
div(ng-show='activePage === PAGES.CREATE_GROUP')
h2.text-center=env.t('createAGroup')
.col-xs-12
+groupCreateForm
br
br
.row
.col-sm-6.col-sm-offset-3
a.btn.btn-primary.btn-lg.btn-block(ng-click="createGroup()", ng-disabled="!newGroupIsReady()")=env.t('create')
div(ng-show='activePage === PAGES.UPGRADE_GROUP')
h2.text-center=env.t('upgradeTitle')
.row.text-center
.col-md-6.col-md-offset-3
a.purchase.btn.btn-primary(ng-click='upgradeGroup(PAYMENTS.STRIPE)')=env.t('card')
a.purchase(ng-click='upgradeGroup(PAYMENTS.AMAZON)')
img(src='https://payments.amazon.com/gp/cba/button', alt=env.t('amazonPayments'))
//- .col-xs-4
//- a.purchase(href='/paypal/subscribe?_id={{user._id}}&apiToken={{User.settings.auth.apiToken}}&sub={{_subscription.key}}{{_subscription.coupon ? "&coupon="+_subscription.coupon : ""}}&groupId={{group.id}}', ng-disabled='!_subscription.key')
//- img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png',alt=env.t('paypal'))
.row
.col-md-6.col-md-offset-3
br
.text-center=env.t('groupSubscriptionPrice')

View File

@@ -0,0 +1,50 @@
// @TODO: This can be a directive and the group/user can be an object passed via attribute
mixin groupSubscription()
div(ng-show="groupPanel == 'subscription'")
.col-md-12
.col-md-12
div(ng-hide="group.purchased.plan.customerId")
+groupPlansBenefits
.row
.col-md-12
br
table.table.alert.alert-info(ng-if='group.purchased.plan.customerId')
tr(ng-if='group.purchased.plan.dateTerminated'): td.alert.alert-warning
span.noninteractive-button.btn-danger=env.t('canceledSubscription')
i.glyphicon.glyphicon-time
| #{env.t('subCanceled')} <strong>{{moment(group.purchased.plan.dateTerminated).format('MM/DD/YYYY')}}</strong>
tr(ng-if='!group.purchased.plan.dateTerminated'): td
h3=env.t('subscribed')
p(ng-if='group.purchased.plan.planId')=env.t('groupSubscriptionPrice')
tr(ng-if='group.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp;#{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
| &nbsp;#{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')

View File

@@ -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')

View File

@@ -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)

View File

@@ -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

View File

@@ -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')

View File

@@ -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')

View File

@@ -0,0 +1,7 @@
script(type='text/ng-template', id='modals/task-edit.html')
.modal-content.task-modal(style='min-width:28em', class='{{taskStatus}}', id="task-{{task._id}}")
.modal-body.text-center(style='padding-bottom:0')
| {{scope}}
include ../tasks/edit/index
.modal-footer(style='margin-top:0')
.container-fluid

View File

@@ -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')

View File

@@ -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')

View File

@@ -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

View File

@@ -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
| &nbsp;
span(ng-show='task.group.approval.requested && !task.group.approval.approved')
span(tooltip=env.t('approvalRequested'))
span=env.t('approvalRequested')
| &nbsp;
// Streak
| &nbsp;
@@ -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'))
| &nbsp;
span.glyphicon.glyphicon-pencil(ng-hide='task._editing')
| &nbsp;
a(ng-hide='!task._editing', ng-click='cancelTaskEdit(task)', tooltip=env.t('cancel'))
span.glyphicon.glyphicon-remove(ng-hide='!task._editing')
| &nbsp;
// save
a(ng-hide='!task._editing', ng-click='saveTask(task)', tooltip=env.t('save'))
span.glyphicon.glyphicon-ok(ng-hide='!task._editing')
| &nbsp;
//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'))
| &nbsp;
// 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
| &nbsp;

View File

@@ -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]')

View File

@@ -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')

View File

@@ -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)'