mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Group managers (#8591)
* Added abiltiy to add group managers * Added ability to remove managers * Added ability for managers to add group tasks * Allower managers to assign tasks * Allowed managers to unassign tasks * Allow managers to delete group tasks * Allowed managers to approve * Added initial ui * Added approval view for managers * Allowed managers to edit * Fixed lint issues * Added spacing to buttons * Removed leader from selection of group managers * Code review updates * Ensured approvals are only done once * Added ability for parties to add managers * Add notifications to all managers when approval is requests * Removed tasks need approval notifications from all managers when task is approve * Fixed linting issues * Hid add managers UI from groups that are not subscribed * Removed let from front end * Fixed issues with post task url params * Fixed string locales * Removed extra limited strings * Added cannotedit tasks function * Added limit fields and notification check by taskId * Localized string and other minor issues * Added manager and leader indicator * Added group notifications refresh on sync * Added close button for group notifications * Removed group approval notifications when manager is removed * Moved leader/manager indicators to after hp * Added manager fields to groups * Spelling and syntax fixes
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /group/:groupId/remove-manager', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let nonManager;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === groupToUpdate._id;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
nonManager = members[0];
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to add member', async () => {
|
||||
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when manager does not exist', async () => {
|
||||
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonManager._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('userIsNotManager'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a leader to remove managers', async () => {
|
||||
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes group approval notifications from a manager that is removed', async () => {
|
||||
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
requiresApproval: true,
|
||||
});
|
||||
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
|
||||
let memberTasks = await leader.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
await nonLeader.sync();
|
||||
|
||||
expect(nonLeader.notifications.length).to.equal(0);
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
});
|
||||
85
test/api/v3/integration/groups/POST-groups_manager.test.js
Normal file
85
test/api/v3/integration/groups/POST-groups_manager.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
generateUser,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
|
||||
describe('POST /group/:groupId/add-manager', () => {
|
||||
let leader, nonLeader, groupToUpdate;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
let nonMember;
|
||||
|
||||
context('Guilds', () => {
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
leader = groupLeader;
|
||||
nonLeader = members[0];
|
||||
nonMember = await generateUser();
|
||||
});
|
||||
|
||||
it('returns an error when a non group leader tries to add member', async () => {
|
||||
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupOnlyLeaderCanUpdate'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when trying to promote a non member', async () => {
|
||||
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonMember._id,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('userMustBeMember'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a leader to add managers', async () => {
|
||||
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
|
||||
managerId: nonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
context('Party', () => {
|
||||
let party, partyLeader, partyNonLeader;
|
||||
|
||||
beforeEach(async () => {
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
party = group;
|
||||
partyLeader = groupLeader;
|
||||
partyNonLeader = members[0];
|
||||
});
|
||||
|
||||
it('allows leader of party to add managers', async () => {
|
||||
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
|
||||
managerId: partyNonLeader._id,
|
||||
});
|
||||
|
||||
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('DELETE /tasks/:id', () => {
|
||||
describe('Groups DELETE /tasks/:id', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
@@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('allows a manager to delete a group task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.del(`/tasks/${task._id}`);
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('taskNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('unlinks assigned user', async () => {
|
||||
await user.del(`/tasks/${task._id}`);
|
||||
|
||||
|
||||
@@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
|
||||
let approvals = await user.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
});
|
||||
|
||||
it('allows managers to get a list of task that need approval', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member._id,
|
||||
});
|
||||
|
||||
let approvals = await member.get(`/approvals/group/${guild._id}`);
|
||||
expect(approvals[0]._id).to.equal(syncedTask._id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/approve/:userId', () => {
|
||||
let user, guild, member, task;
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
@@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('allows a manager to approve an assigned user', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await member.sync();
|
||||
|
||||
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', {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(member2._id);
|
||||
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('removes approval pending notifications from managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(0);
|
||||
expect(member2.notifications.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('prevents double approval on a task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('canOnlyApproveTaskOnce'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
let user, guild, member, task;
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
return memberTask.group.id === guild._id;
|
||||
@@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
name: 'Test Guild',
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
member = members[0];
|
||||
member2 = members[1];
|
||||
|
||||
task = await user.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test todo',
|
||||
@@ -56,6 +57,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
|
||||
@@ -63,6 +65,42 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
});
|
||||
|
||||
it('sends notifications to all managers', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('taskApprovalHasBeenRequested'),
|
||||
});
|
||||
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(user.notifications[0].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
}));
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('errors when approval has already been requested', async () => {
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import {
|
||||
generateUser,
|
||||
generateGroup,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-v3-integration.helper';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
describe('POST /tasks/group/:groupid', () => {
|
||||
let user, guild;
|
||||
let user, guild, manager;
|
||||
let groupName = 'Test Public Guild';
|
||||
let groupType = 'guild';
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({balance: 1});
|
||||
guild = await generateGroup(user, {type: 'guild'});
|
||||
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
user = groupLeader;
|
||||
manager = members[0];
|
||||
});
|
||||
|
||||
it('returns error when group is not found', async () => {
|
||||
@@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
|
||||
expect(task.everyX).to.eql(5);
|
||||
expect(new Date(task.startDate)).to.eql(now);
|
||||
});
|
||||
|
||||
it('allows a manager to add a group task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: manager._id,
|
||||
});
|
||||
|
||||
let task = await manager.post(`/tasks/group/${guild._id}`, {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
up: false,
|
||||
down: true,
|
||||
notes: 1976,
|
||||
});
|
||||
|
||||
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(groupTask[0].group.id).to.equal(guild._id);
|
||||
expect(task.text).to.eql('test habit');
|
||||
expect(task.notes).to.eql('1976');
|
||||
expect(task.type).to.eql('habit');
|
||||
expect(task.up).to.eql(false);
|
||||
expect(task.down).to.eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:taskId', () => {
|
||||
describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
let user, guild, member, member2, task;
|
||||
|
||||
function findAssignedTask (memberTask) {
|
||||
@@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
|
||||
expect(member1SyncedTask).to.exist;
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('allows a manager to assign tasks', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
|
||||
|
||||
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
|
||||
expect(syncedTask).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
|
||||
expect(member2SyncedTask).to.exist;
|
||||
});
|
||||
|
||||
it('allows a manager to unassign a user from a task', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
|
||||
expect(syncedTask).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
|
||||
expect(member2SyncedTask.up).to.eql(false);
|
||||
expect(member2SyncedTask.down).to.eql(false);
|
||||
});
|
||||
|
||||
it('updates the linked tasks', async () => {
|
||||
await user.post(`/groups/${guild._id}/add-manager`, {
|
||||
managerId: member2._id,
|
||||
});
|
||||
|
||||
await member2.put(`/tasks/${task._id}`, {
|
||||
text: 'some new text',
|
||||
up: false,
|
||||
down: false,
|
||||
notes: 'some new notes',
|
||||
});
|
||||
|
||||
|
||||
let memberTasks = await member.get('/tasks/user');
|
||||
let syncedTask = find(memberTasks, findAssignedTask);
|
||||
|
||||
expect(syncedTask.text).to.eql('some new text');
|
||||
expect(syncedTask.up).to.eql(false);
|
||||
expect(syncedTask.down).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -215,3 +215,6 @@ group-members-autocomplete
|
||||
background #ff6633
|
||||
color #fff
|
||||
cursor pointer
|
||||
|
||||
.add-manager-button, .remove-manager-button
|
||||
margin-left: 1rem;
|
||||
|
||||
@@ -160,8 +160,10 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', '$ro
|
||||
|
||||
$scope.checkGroupAccess = function (group) {
|
||||
if (!group || !group.leader) return true;
|
||||
if (User.user._id !== group.leader._id) return false;
|
||||
return true;
|
||||
var userId = User.user._id;
|
||||
var leader = group.leader._id === userId;
|
||||
var isManager = Boolean(group.managers[userId]);
|
||||
return leader || isManager;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -128,5 +128,37 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
|
||||
.then(function (response) {
|
||||
$rootScope.openModal('private-message', {controller: 'MemberModalCtrl'});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.memberProfileName = function (memberId) {
|
||||
var member = _.find($scope.groupCopy.members, function (member) { return member._id === memberId; });
|
||||
return member.profile.name;
|
||||
};
|
||||
|
||||
$scope.addManager = function () {
|
||||
Groups.Group.addManager($scope.groupCopy._id, $scope.groupCopy._newManager)
|
||||
.then(function (response) {
|
||||
$scope.groupCopy._newManager = '';
|
||||
$scope.groupCopy.managers = response.data.data.managers;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeManager = function (memberId) {
|
||||
Groups.Group.removeManager($scope.groupCopy._id, memberId)
|
||||
.then(function (response) {
|
||||
$scope.groupCopy._newManager = '';
|
||||
$scope.groupCopy.managers = response.data.data.managers;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isManager = function (memberId, group) {
|
||||
return Boolean(group.managers[memberId]);
|
||||
}
|
||||
|
||||
$scope.userCanApprove = function (userId, group) {
|
||||
if (!group) return false;
|
||||
var leader = group.leader._id === userId;
|
||||
var userIsManager = !!group.managers[userId];
|
||||
return leader || userIsManager;
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -116,10 +116,10 @@ angular.module('habitrpg')
|
||||
return selectNotificationValue(false, false, false, false, false, true, false, false);
|
||||
};
|
||||
|
||||
$scope.viewGroupApprovalNotification = function (notification, $index) {
|
||||
$scope.viewGroupApprovalNotification = function (notification, $index, navigate) {
|
||||
User.readNotification(notification.id);
|
||||
User.user.groupNotifications.splice($index, 1);
|
||||
$state.go("options.social.guilds.detail", {gid: notification.data.groupId});
|
||||
if (navigate) $state.go("options.social.guilds.detail", {gid: notification.data.groupId});
|
||||
};
|
||||
|
||||
$scope.groupApprovalNotificationIcon = function (notification) {
|
||||
|
||||
@@ -89,14 +89,20 @@ habitrpg.controller('NotificationCtrl',
|
||||
var notificationsToRead = [];
|
||||
var scoreTaskNotification;
|
||||
|
||||
User.user.groupNotifications = []; // Flush group notifictions
|
||||
|
||||
after.forEach(function (notification) {
|
||||
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastShownNotifications.push(notification.id);
|
||||
if (lastShownNotifications.length > 10) {
|
||||
lastShownNotifications.splice(0, 9);
|
||||
// Some notifications are not marked read here, so we need to fix this system
|
||||
// to handle notifications differently
|
||||
if (['GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVAL'].indexOf(notification.type) === -1) {
|
||||
lastShownNotifications.push(notification.id);
|
||||
if (lastShownNotifications.length > 10) {
|
||||
lastShownNotifications.splice(0, 9);
|
||||
}
|
||||
}
|
||||
|
||||
var markAsRead = true;
|
||||
|
||||
@@ -108,6 +108,26 @@ angular.module('habitrpg')
|
||||
});
|
||||
};
|
||||
|
||||
Group.addManager = function(gid, memberId) {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: groupApiURLPrefix + '/' + gid + '/add-manager/',
|
||||
data: {
|
||||
managerId: memberId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Group.removeManager = function(gid, memberId) {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: groupApiURLPrefix + '/' + gid + '/remove-manager/',
|
||||
data: {
|
||||
managerId: memberId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
$rootScope.$on('syncPartyRequest', function (event, options) {
|
||||
if (options.type === 'user_update') {
|
||||
var index = _.findIndex(data.party.members, function(user) { return user._id === options.user._id; });
|
||||
|
||||
@@ -272,5 +272,13 @@
|
||||
"confirmCancelGroupPlan": "Are you sure you want to cancel the group plan and remove its benefits from all members, including their free subscriptions?",
|
||||
"canceledGroupPlan": "Canceled Group Plan",
|
||||
"groupPlanCanceled": "Group Plan will become inactive on",
|
||||
"purchasedGroupPlanPlanExtraMonths": "You have <%= months %> months of extra group plan credit."
|
||||
"purchasedGroupPlanPlanExtraMonths": "You have <%= months %> months of extra group plan credit.",
|
||||
"addManagers": "Add Managers",
|
||||
"addManager": "Add Manager",
|
||||
"removeManager": "Remove",
|
||||
"userMustBeMember": "User must be a member",
|
||||
"userIsNotManager": "User is not manager",
|
||||
"canOnlyApproveTaskOnce": "This task has already been approved.",
|
||||
"leaderMarker": " - Leader",
|
||||
"managerMarker": " - Manager"
|
||||
}
|
||||
|
||||
@@ -1095,4 +1095,108 @@ api.inviteToGroup = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/groups/:groupId/add-manager Add a manager to a group
|
||||
* @apiName AddGroupManager
|
||||
* @apiGroup Group
|
||||
*
|
||||
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
*
|
||||
* @apiParamExample {String} party:
|
||||
* /api/v3/groups/party/add-manager
|
||||
*
|
||||
* @apiBody (Body) {UUID} managerId The user _id of the member to promote to manager
|
||||
*
|
||||
* @apiSuccess {Object} data An empty object
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} managerId req.body.managerId is required
|
||||
* @apiUse groupIdRequired
|
||||
*/
|
||||
api.addGroupManager = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/add-manager',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let managerId = req.body.managerId;
|
||||
|
||||
req.checkParams('groupId', apiMessages('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
req.checkBody('managerId', apiMessages('managerIdRequired')).notEmpty();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let newManager = await User.findById(managerId, 'guilds party').exec();
|
||||
let groupFields = basicGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: groupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
|
||||
let isMember = group.isMember(newManager);
|
||||
if (!isMember) throw new NotAuthorized(res.t('userMustBeMember'));
|
||||
|
||||
group.managers[managerId] = true;
|
||||
group.markModified('managers');
|
||||
await group.save();
|
||||
|
||||
res.respond(200, group);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/groups/:groupId/remove-manager Remove a manager from a group
|
||||
* @apiName RemoveGroupManager
|
||||
* @apiGroup Group
|
||||
*
|
||||
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
|
||||
*
|
||||
* @apiParamExample {String} party:
|
||||
* /api/v3/groups/party/add-manager
|
||||
*
|
||||
* @apiBody (Body) {UUID} managerId The user _id of the member to remove
|
||||
*
|
||||
* @apiSuccess {Object} group The group
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} managerId req.body.managerId is required
|
||||
* @apiUse groupIdRequired
|
||||
*/
|
||||
api.removeGroupManager = {
|
||||
method: 'POST',
|
||||
url: '/groups/:groupId/remove-manager',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let managerId = req.body.managerId;
|
||||
|
||||
req.checkParams('groupId', apiMessages('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
|
||||
req.checkBody('managerId', apiMessages('managerIdRequired')).notEmpty();
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
let groupFields = basicGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: groupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
|
||||
|
||||
if (!group.managers[managerId]) throw new NotAuthorized(res.t('userIsNotManager'));
|
||||
|
||||
delete group.managers[managerId];
|
||||
group.markModified('managers');
|
||||
await group.save();
|
||||
|
||||
let manager = await User.findById(managerId, 'notifications').exec();
|
||||
let newNotifications = manager.notifications.filter((notification) => {
|
||||
return notification.type !== 'GROUP_TASK_APPROVAL';
|
||||
});
|
||||
manager.notifications = newNotifications;
|
||||
manager.markModified('notifications');
|
||||
await manager.save();
|
||||
|
||||
res.respond(200, group);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
|
||||
@@ -25,6 +25,13 @@ import logger from '../../libs/logger';
|
||||
|
||||
const MAX_SCORE_NOTES_LENGTH = 256;
|
||||
|
||||
function canNotEditTasks (group, user, assignedUserId) {
|
||||
let isNotGroupLeader = group.leader !== user._id;
|
||||
let isManager = Boolean(group.managers[user._id]);
|
||||
let userIsAssigningToSelf = Boolean(assignedUserId && user._id === assignedUserId);
|
||||
return isNotGroupLeader && !isManager && !userIsAssigningToSelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiDefine TaskNotFound
|
||||
* @apiError (404) {NotFound} TaskNotFound The specified task could not be found.
|
||||
@@ -413,9 +420,10 @@ api.updateTask = {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.group.id && !task.userId) {
|
||||
// @TODO: Abstract this access snippet
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -530,18 +538,29 @@ api.scoreTask = {
|
||||
task.group.approval.requested = true;
|
||||
task.group.approval.requestedDate = new Date();
|
||||
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let groupLeader = await User.findById(group.leader).exec(); // Use this method so we can get access to notifications
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
|
||||
groupLeader.addNotification('GROUP_TASK_APPROVAL', {
|
||||
message: res.t('userHasRequestedTaskApproval', {
|
||||
user: user.profile.name,
|
||||
taskName: task.text,
|
||||
}, groupLeader.preferences.language),
|
||||
groupId: group._id,
|
||||
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated
|
||||
let managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
let managers = await User.find({_id: managerIds}, 'notifications preferences').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
let managerPromises = [];
|
||||
managers.forEach((manager) => {
|
||||
manager.addNotification('GROUP_TASK_APPROVAL', {
|
||||
message: res.t('userHasRequestedTaskApproval', {
|
||||
user: user.profile.name,
|
||||
taskName: task.text,
|
||||
}, manager.preferences.language),
|
||||
groupId: group._id,
|
||||
taskId: task._id,
|
||||
});
|
||||
managerPromises.push(manager.save());
|
||||
});
|
||||
|
||||
await Bluebird.all([groupLeader.save(), task.save()]);
|
||||
managerPromises.push(task.save());
|
||||
await Bluebird.all(managerPromises);
|
||||
|
||||
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
|
||||
}
|
||||
@@ -694,9 +713,9 @@ api.addChecklistItem = {
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.group.id && !task.userId) {
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -803,9 +822,10 @@ api.updateChecklistItem = {
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.group.id && !task.userId) {
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -867,9 +887,10 @@ api.removeChecklistItem = {
|
||||
if (!task) {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.group.id && !task.userId) {
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||
@@ -1185,9 +1206,10 @@ api.deleteTask = {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
} else if (task.group.id && !task.userId) {
|
||||
// @TODO: Abstract this access snippet
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
await group.removeTask(task);
|
||||
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
|
||||
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import Bluebird from 'bluebird';
|
||||
import * as Tasks from '../../../models/task';
|
||||
@@ -16,6 +17,14 @@ import {
|
||||
|
||||
let requiredGroupFields = '_id leader tasksOrder name';
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
|
||||
function canNotEditTasks (group, user, assignedUserId) {
|
||||
let isNotGroupLeader = group.leader !== user._id;
|
||||
let isManager = Boolean(group.managers[user._id]);
|
||||
let userIsAssigningToSelf = Boolean(assignedUserId && user._id === assignedUserId);
|
||||
return isNotGroupLeader && !isManager && !userIsAssigningToSelf;
|
||||
}
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
@@ -40,10 +49,11 @@ api.createGroupTasks = {
|
||||
|
||||
let user = res.locals.user;
|
||||
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
let tasks = await createTasks(req, res, {user, group});
|
||||
|
||||
@@ -171,11 +181,11 @@ api.assignTask = {
|
||||
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
||||
}
|
||||
|
||||
let groupFields = `${requiredGroupFields} chat`;
|
||||
let groupFields = `${requiredGroupFields} chat managers`;
|
||||
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'));
|
||||
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
// User is claiming the task
|
||||
if (user._id === assignedUserId) {
|
||||
@@ -229,10 +239,11 @@ api.unassignTask = {
|
||||
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
|
||||
}
|
||||
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
await group.unlinkTask(task, assignedUser);
|
||||
|
||||
@@ -277,10 +288,12 @@ api.approveTask = {
|
||||
throw new NotFound(res.t('taskNotFound'));
|
||||
}
|
||||
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId: task.group.id, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
|
||||
|
||||
task.group.approval.dateApproved = new Date();
|
||||
task.group.approval.approvingUser = user._id;
|
||||
@@ -296,7 +309,25 @@ api.approveTask = {
|
||||
scoreTask: task,
|
||||
});
|
||||
|
||||
await Bluebird.all([assignedUser.save(), task.save()]);
|
||||
let managerIds = Object.keys(group.managers);
|
||||
managerIds.push(group.leader);
|
||||
let managers = await User.find({_id: managerIds}, 'notifications').exec(); // Use this method so we can get access to notifications
|
||||
|
||||
let managerPromises = [];
|
||||
managers.forEach((manager) => {
|
||||
let notificationIndex = findIndex(manager.notifications, function findNotification (notification) {
|
||||
return notification.data.taskId === task._id;
|
||||
});
|
||||
|
||||
if (notificationIndex !== -1) {
|
||||
manager.notifications.splice(notificationIndex, 1);
|
||||
managerPromises.push(manager.save());
|
||||
}
|
||||
});
|
||||
|
||||
managerPromises.push(task.save());
|
||||
managerPromises.push(assignedUser.save());
|
||||
await Bluebird.all(managerPromises);
|
||||
|
||||
res.respond(200, task);
|
||||
},
|
||||
@@ -325,10 +356,11 @@ api.getGroupApprovals = {
|
||||
let user = res.locals.user;
|
||||
let groupId = req.params.groupId;
|
||||
|
||||
let group = await Group.getGroup({user, groupId, fields: requiredGroupFields});
|
||||
let fields = requiredGroupFields.concat(' managers');
|
||||
let group = await Group.getGroup({user, groupId, fields});
|
||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||
|
||||
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
|
||||
|
||||
let approvals = await Tasks.Task.find({
|
||||
'group.id': groupId,
|
||||
|
||||
@@ -8,6 +8,8 @@ const messages = {
|
||||
guildsOnlyPaginate: 'Only public guilds support pagination.',
|
||||
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
|
||||
guildsPageInteger: 'req.query.page must be an integer greater than or equal to 0.',
|
||||
groupIdRequired: 'req.params.groupId must contain a groupId.',
|
||||
managerIdRequired: 'req.body.managerId must contain a user ID.',
|
||||
};
|
||||
|
||||
export default function (msgKey, vars = {}) {
|
||||
|
||||
@@ -105,13 +105,16 @@ export let schema = new Schema({
|
||||
return {};
|
||||
}},
|
||||
},
|
||||
managers: {type: Schema.Types.Mixed, default: () => {
|
||||
return {};
|
||||
}},
|
||||
}, {
|
||||
strict: true,
|
||||
minimize: false, // So empty objects are returned
|
||||
});
|
||||
|
||||
schema.plugin(baseModel, {
|
||||
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased'],
|
||||
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased', 'managers'],
|
||||
private: ['purchased.plan'],
|
||||
toJSONTransform (plainObj, originalDoc) {
|
||||
if (plainObj.purchased) plainObj.purchased.active = originalDoc.isSubscribed();
|
||||
|
||||
@@ -14,7 +14,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
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')
|
||||
li(ng-show='group.purchased.active && userCanApprove(user._id, group)')
|
||||
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('paymentDetails')
|
||||
@@ -65,6 +65,17 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
h4=env.t('assignLeader')
|
||||
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
|
||||
|
||||
div(ng-if='group.purchased.active')
|
||||
h4=env.t('addManagers')
|
||||
.form-group
|
||||
select#group-leader-selection(ng-model='groupCopy._newManager')
|
||||
option(ng-repeat='member in group.members', ng-if='member._id !== group.leader.id', ng-value='member._id') {{member.profile.name}}
|
||||
button.btn.btn-primary.add-manager-button(ng-click='addManager()')=env.t('addManager')
|
||||
ul
|
||||
li(ng-repeat='(managerId, value) in groupCopy.managers')
|
||||
| {{memberProfileName(managerId)}}
|
||||
button.btn.btn-warning.remove-manager-button(ng-click='removeManager(managerId)')=env.t('removeManager')
|
||||
|
||||
div(ng-show='!group._editing')
|
||||
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
|
||||
markdown(text='group.description')
|
||||
@@ -105,6 +116,10 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
| {{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') : ''}}
|
||||
span(ng-if='group.leader._id === member.id')
|
||||
| {{env.t('leaderMarker')}}
|
||||
span(ng-show='isManager(member._id, group)')
|
||||
| {{env.t('managerMarker')}}
|
||||
.pull-right(ng-if='group.type === "party"')
|
||||
span.text-success {{member.online ? '● ' + env.t('online') : ''}}
|
||||
tr(ng-if='::group.memberCount > group.members.length')
|
||||
@@ -178,6 +193,6 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
|
||||
|
||||
group-tasks(ng-show="groupPanel == 'tasks'")
|
||||
|
||||
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
|
||||
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="userCanApprove(user._id, group)", group="group")
|
||||
|
||||
+groupSubscription
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
script(type='text/ng-template', id='partials/groups.tasks.actions.html')
|
||||
div(ng-if="group.leader._id === user._id", class="col-md-12")
|
||||
div(ng-if="group.leader._id === user._id || group.managers[user._id]", class="col-md-12")
|
||||
strong=env.t('assignTask')
|
||||
group-members-autocomplete(ng-model="assignedMembers")
|
||||
|
||||
|
||||
@@ -223,10 +223,16 @@ nav.toolbar(ng-controller='MenuCtrl')
|
||||
a(ng-click='clearMessages(k)', popover=env.t('clear'),popover-placement='right',popover-trigger='mouseenter',popover-append-to-body='true')
|
||||
span.glyphicon.glyphicon-remove-circle
|
||||
li(ng-repeat='notification in user.groupNotifications')
|
||||
a(ng-click='viewGroupApprovalNotification(notification, $index)', data-close-menu)
|
||||
a(ng-click='viewGroupApprovalNotification(notification, $index, true)', data-close-menu)
|
||||
span(class="{{::groupApprovalNotificationIcon(notification)}}")
|
||||
span
|
||||
| {{notification.data.message}}
|
||||
a(ng-click='viewGroupApprovalNotification(notification, $index)',
|
||||
popover=env.t('clear'),
|
||||
popover-placement='right',
|
||||
popover-trigger='mouseenter',
|
||||
popover-append-to-body='true')
|
||||
span.glyphicon.glyphicon-remove-circle
|
||||
|
||||
ul.toolbar-controls
|
||||
li.toolbar-controls-button
|
||||
|
||||
Reference in New Issue
Block a user