Merge branch 'develop' into release

This commit is contained in:
Sabe Jones
2018-07-26 18:15:33 +00:00
34 changed files with 490 additions and 75 deletions

View File

@@ -140,4 +140,89 @@ describe('POST /tasks/:id/approve/:userId', () => {
message: t('canOnlyApproveTaskOnce'),
});
});
it('completes master task when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is approved', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let member2Tasks = await member2.get('/tasks/user');
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are approved if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are approved if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: true,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/approve/${member2._id}`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -125,7 +125,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
});
it('allows a user to score an apporoved task', async () => {
it('allows a user to score an approved task', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
@@ -137,4 +137,112 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.completed).to.equal(true);
expect(updatedTask.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('completes master task when single-completion task is completed', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
it('deletes other assigned user tasks when single-completion task is completed', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'singleCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let member2Tasks = await member2.get('/tasks/user');
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
expect(syncedTask2).to.equal(undefined);
});
it('does not complete master task when not all user tasks are completed if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(false);
});
it('completes master task when all user tasks are completed if all assigned must complete', async () => {
let sharedCompletionTask = await user.post(`/tasks/group/${guild._id}`, {
text: 'shared completion todo',
type: 'todo',
requiresApproval: false,
sharedCompletion: 'allAssignedCompletion',
});
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member._id}`);
await user.post(`/tasks/${sharedCompletionTask._id}/assign/${member2._id}`);
let memberTasks = await member.get('/tasks/user');
let member2Tasks = await member2.get('/tasks/user');
let syncedTask = find(memberTasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
let syncedTask2 = find(member2Tasks, (memberTask) => {
return memberTask.group.taskId === sharedCompletionTask._id;
});
await member.post(`/tasks/${syncedTask._id}/score/up`);
await member2.post(`/tasks/${syncedTask2._id}/score/up`);
let groupTasks = await user.get(`/tasks/group/${guild._id}?type=completedTodos`);
let masterTask = find(groupTasks, (groupTask) => {
return groupTask._id === sharedCompletionTask._id;
});
expect(masterTask.completed).to.equal(true);
});
});

View File

@@ -105,7 +105,7 @@ div
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal, .modal-open {
.modal {
overflow-y: scroll !important;
}
@@ -499,8 +499,16 @@ export default {
});
this.$root.$on('bv::modal::hidden', (bvEvent) => {
const modalId = bvEvent.target && bvEvent.target.id;
if (!modalId) return;
let modalId = bvEvent.target && bvEvent.target.id;
// sometimes the target isn't passed to the hidden event, fallback is the vueTarget
if (!modalId) {
modalId = bvEvent.vueTarget && bvEvent.vueTarget.id;
}
if (!modalId) {
return;
}
const modalStack = this.$store.state.modalStack;
@@ -517,6 +525,7 @@ export default {
// Get previous modal
const modalBefore = modalOnTop ? modalOnTop.prev : undefined;
if (modalBefore) this.$root.$emit('bv::show::modal', modalBefore, {fromRoot: true});
});
},

View File

@@ -1,13 +1,22 @@
.markdown {
> p {
margin-bottom: 0px;
p {
margin-bottom: 8px;
}
h1 {
margin-bottom: 10px;
margin-top: 14px;
line-height: 1.17;
}
h2 {
margin-bottom: 6px;
margin-top: 10px;
}
h3 {
margin-bottom: 4px;
margin-top: 6px;
color: $gray-10;
}

View File

@@ -317,6 +317,8 @@ export default {
methods: {
async shown () {
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
if (this.user.party && this.user.party._id) {
await this.$store.dispatch('party:getParty');
const party = this.$store.state.party.data;
if (party._id) {
@@ -326,6 +328,7 @@ export default {
privacy: 'private',
});
}
}
this.groups.push({
name: this.$t('publicChallengesTitle'),

View File

@@ -11,7 +11,7 @@ div
)
| {{msg.user}}
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
p.time {{msg.timestamp | timeAgo}}
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
.text(v-markdown='msg.text')
hr
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
@@ -72,6 +72,7 @@ div
.time {
font-size: 12px;
color: #878190;
width: 150px;
}
.text {
@@ -165,7 +166,7 @@ export default {
return moment(value).fromNow();
},
date (value) {
// @TODO: Add user preference
// @TODO: Vue doesn't support this so we cant user preference
return moment(value).toDate();
},
},

View File

@@ -80,6 +80,7 @@
:key="column",
:taskListOverride='tasksByType[column]',
v-on:editTask="editTask",
v-on:loadGroupCompletedTodos="loadGroupCompletedTodos",
:group='group',
:searchText="searchText")
</template>
@@ -384,6 +385,20 @@ export default {
this.$root.$emit('bv::show::modal', 'task-modal');
});
},
async loadGroupCompletedTodos () {
const completedTodos = await this.$store.dispatch('tasks:getCompletedGroupTasks', {
groupId: this.searchId,
});
completedTodos.forEach((task) => {
const existingTaskIndex = findIndex(this.tasksByType.todo, (todo) => {
return todo._id === task._id;
});
if (existingTaskIndex === -1) {
this.tasksByType.todo.push(task);
}
});
},
createTask (type) {
this.taskFormPurpose = 'create';
this.creatingTask = taskDefaults({type, text: ''});

View File

@@ -19,19 +19,20 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
p(v-once) {{$t('wantToJoinPartyDescription')}}
button.btn.btn-primary(v-once, @click='shareUserId()') {{$t('shartUserId')}}
.share-userid-options(v-if="shareUserIdShown")
.option-item(v-once)
.option-item(@click='copyUserId()')
.svg-icon(v-html="icons.copy")
input(type="text", v-model="user._id", id="userIdInput")
| Copy User ID
.option-item(v-once)
//.option-item(v-once)
.svg-icon(v-html="icons.greyBadge")
| {{$t('lookingForGroup')}}
.option-item(v-once)
//.option-item(v-once)
.svg-icon(v-html="icons.qrCode")
| {{$t('qrCode')}}
.option-item(v-once)
//.option-item(v-once)
.svg-icon.facebook(v-html="icons.facebook")
| Facebook
.option-item(v-once)
//.option-item(v-once)
.svg-icon(v-html="icons.twitter")
| Twitter
</template>
@@ -108,10 +109,15 @@ b-modal#create-party-modal(title="Empty", size='lg', hide-footer=true)
border-radius: 2px;
width: 220px;
position: absolute;
top: -8em;
top: 9em;
left: 4.8em;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
#userIdInput {
position: absolute;
left: 1000rem;
}
.option-item {
padding: 1em;
@@ -183,6 +189,12 @@ export default {
this.$root.$emit('bv::hide::modal', 'create-party-modal');
this.$router.push('/party');
},
copyUserId () {
const copyText = document.getElementById('userIdInput');
copyText.select();
document.execCommand('copy');
alert('User ID has been copied');
},
},
};
</script>

View File

@@ -76,10 +76,18 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
.gold {
color: #fdbb5a;
.member-count {
color: #fdbb5a;
}
}
.silver {
color: #c2c2c2;
.member-count {
color: #c2c2c2;
}
}
.badge-column {

View File

@@ -8,8 +8,8 @@ menu-dropdown.item-user(:right="true")
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
h3 {{ user.profile.name }}
span.small-text {{ $t('editAvatar') }}
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
| {{ $t('messages') }}
a.nav-link.dropdown-item.dropdown-separated.d-flex.justify-content-between.align-items-center(@click.prevent='showInbox()')
div {{ $t('messages') }}
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
a.dropdown-item(@click='showAvatar("backgrounds", "2018")') {{ $t('backgrounds') }}
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}

View File

@@ -465,7 +465,7 @@ export default {
this.$root.$emit('bv::show::modal', 'won-challenge');
break;
case 'STREAK_ACHIEVEMENT':
this.streak(this.user.achievements.streak);
this.text(this.user.achievements.streak);
this.playSound('Achievement_Unlocked');
if (!this.user.preferences.suppressModals.streak) {
this.$root.$emit('bv::show::modal', 'streak');
@@ -485,7 +485,9 @@ export default {
break;
case 'CHALLENGE_JOINED_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.text(`${this.$t('achievement')}: ${this.$t('joinedChallenge')}`, () => {
this.$root.$emit('bv::show::modal', 'joined-challenge');
}, false);
break;
case 'INVITED_FRIEND_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');

View File

@@ -3,8 +3,10 @@
h2.text-center Continue with Amazon
#AmazonPayButton
#AmazonPayWallet(v-if="amazonPayments.loggedIn", style="width: 400px; height: 228px;")
#AmazonPayRecurring(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'",
style="width: 400px; height: 140px;")
template(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'")
br
p(v-html="$t('amazonPaymentsRecurring')")
#AmazonPayRecurring(style="width: 400px; height: 140px;")
.modal-footer
.text-center
button.btn.btn-primary(v-if="amazonPaymentsCanCheckout",

View File

@@ -1,5 +1,5 @@
<template lang="pug">
b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
b-modal#send-gems(:title="title", :hide-footer="true", size='lg', @hide='onHide()')
.modal-body(v-if='userReceivingGems')
.panel.panel-default(
:class="gift.type === 'gems' ? 'panel-primary' : 'transparent'",
@@ -138,6 +138,9 @@ export default {
this.text(this.$t('sentGems'));
this.close();
},
onHide () {
this.gift.message = '';
},
close () {
this.$root.$emit('bv::hide::modal', 'send-gems');
},

View File

@@ -1,6 +1,6 @@
<template lang="pug">
transition(name="fade")
.notification.callout.animated(:class="classes", v-if='show', @click='show = false')
.notification.callout.animated(:class="classes", v-if='show', @click='handleOnClick()')
.row(v-if='notification.type === "error"')
.text.col-12
div(v-html='notification.text')
@@ -146,6 +146,15 @@ export default {
beforeDestroy () {
clearTimeout(this.timer);
},
methods: {
handleOnClick () {
if (typeof this.notification.onClick === 'function') {
this.notification.onClick();
}
this.show = false;
},
},
watch: {
show () {
this.$store.dispatch('snackbars:remove', this.notification);

View File

@@ -362,7 +362,7 @@ export default {
type: this.type,
filterType: this.activeFilter.label,
}) :
this.taskListOverride;
this.filterByCompleted(this.taskListOverride, this.activeFilter.label);
let taggedList = this.filterByTagList(filteredTaskList, this.selectedTags);
let searchedList = this.filterBySearchText(taggedList, this.searchText);
@@ -556,8 +556,12 @@ export default {
activateFilter (type, filter = '') {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
if (this.group && this.group._id) {
this.$emit('loadGroupCompletedTodos');
} else {
this.loadCompletedTodos();
}
}
// the only time activateFilter is called with filter==='' is when the component is first created
// this can be used to check If the user has set 'due' as default filter for daily
@@ -594,6 +598,13 @@ export default {
}
});
},
filterByCompleted (taskList, filter) {
if (!taskList) return [];
return taskList.filter(task => {
if (filter === 'complete2') return task.completed;
return !task.completed;
});
},
filterByTagList (taskList, tagList = []) {
let filteredTaskList = taskList;
// filter requested tasks by tags

View File

@@ -185,6 +185,15 @@
:checked="requiresApproval",
@change="updateRequiresApproval"
)
.form-group(v-if="task.type === 'todo'")
label(v-once) {{ $t('sharedCompletion') }}
b-dropdown.inline-dropdown(:text="$t(sharedCompletion)")
b-dropdown-item(
v-for="completionOption in ['recurringCompletion', 'singleCompletion', 'allAssignedCompletion']",
:key="completionOption",
@click="sharedCompletion = completionOption",
:class="{active: sharedCompletion === completionOption}"
) {{ $t(completionOption) }}
.advanced-settings(v-if="task.type !== 'reward'")
.advanced-settings-toggle.d-flex.justify-content-between.align-items-center(@click = "showAdvancedOptions = !showAdvancedOptions")
@@ -691,6 +700,7 @@ export default {
calendar: calendarIcon,
}),
requiresApproval: false, // We can't set task.group fields so we use this field to toggle
sharedCompletion: 'recurringCompletion',
members: [],
memberNamesById: {},
assignedMembers: [],
@@ -811,6 +821,7 @@ export default {
});
this.assignedMembers = [];
if (this.task.group && this.task.group.assignedUsers) this.assignedMembers = this.task.group.assignedUsers;
if (this.task.group) this.sharedCompletion = this.task.group.sharedCompletion || 'recurringCompletion';
}
// @TODO: This whole component is mutating a prop and that causes issues. We need to not copy the prop similar to group modals
@@ -892,10 +903,13 @@ export default {
async submit () {
if (this.newChecklistItem) this.addChecklistItem();
// TODO Fix up permissions on task.group so we don't have to keep doing these hacks
if (this.groupId) {
this.task.group.assignedUsers = this.assignedMembers;
this.task.requiresApproval = this.requiresApproval;
this.task.group.approval.required = this.requiresApproval;
this.task.sharedCompletion = this.sharedCompletion;
this.task.group.sharedCompletion = this.sharedCompletion;
}
if (this.purpose === 'create') {

View File

@@ -8,7 +8,7 @@ div
button.btn.btn-secondary.gift-icon(@click='openSendGemsModal()', v-b-tooltip.hover.bottom="$t('sendGems')")
.svg-icon.gift-icon(v-html="icons.gift")
button.btn.btn-secondary.remove-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) === -1',
@click="blockUser()", v-b-tooltip.hover.right="$t('block')")
@click="blockUser()", v-b-tooltip.hover.right="$t('blockWarning')")
.svg-icon.remove-icon(v-html="icons.remove")
button.btn.btn-secondary.positive-icon(v-if='user._id !== this.userLoggedIn._id && userLoggedIn.inbox.blocks.indexOf(user._id) !== -1',
@click="unblockUser()", v-b-tooltip.hover.right="$t('unblock')")

View File

@@ -122,7 +122,7 @@
.col-12.col-md-6
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
h3
| {{$t('pointsAvailable')}}
| {{$t('statPoints')}}
.counter.badge(v-if='user.stats.points || userLevel100Plus')
| {{user.stats.points}}&nbsp;
.col-12.col-md-6
@@ -135,16 +135,16 @@
.row
.col-12.col-md-3(v-for='(statInfo, stat) in allocateStatsList')
.box.white.row.col-12
.col-12.col-md-9
.col-9
div(:class='stat') {{ $t(stats[stat].title) }}
.number {{ user.stats[stat] }}
.points {{$t('pts')}}
.col-12.col-md-3
.col-3
div
.up(v-if='user.stats.points', @click='allocate(stat)')
.up(v-if='showStatsSave', @click='allocate(stat)')
div
.down(@click='deallocate(stat)', v-if='user.stats.points')
.row.save-row
.down(v-if='showStatsSave', @click='deallocate(stat)')
.row.save-row(v-if='showStatsSave')
.col-12.col-md-6.offset-md-3.text-center
button.btn.btn-primary(@click='saveAttributes()', :disabled='loading') {{ this.loading ? $t('loading') : $t('save') }}
</template>
@@ -238,6 +238,10 @@
userLevel100Plus () {
return this.user.stats.lvl >= 100;
},
showStatsSave () {
const statsAreBeingUpdated = Object.values(this.statUpdates).find(stat => stat > 0);
return Boolean(this.user.stats.points) || statsAreBeingUpdated;
},
},
methods: {
getGearTitle (key) {

View File

@@ -1,13 +1,27 @@
export default {
methods: {
makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
this.$store.dispatch('shops:genericPurchase', {
async makeGenericPurchase (item, type = 'buyModal', quantity = 1) {
try {
await this.$store.dispatch('shops:genericPurchase', {
pinType: item.pinType,
type: item.purchaseType,
key: item.key,
currency: item.currency,
quantity,
});
} catch (e) {
if (!e.request) {
// axios request errors already handled by app.vue
this.$store.dispatch('snackbars:add', {
title: '',
text: e.message,
type: 'error',
});
return;
} else {
throw e;
}
}
this.$root.$emit('playSound', 'Reward');

View File

@@ -56,9 +56,9 @@ export default {
streak (val) {
this.notify(`${val}`, 'streak');
},
text (val, onClick) {
text (val, onClick, timeout) {
if (!val) return;
this.notify(val, 'info', null, null, onClick);
this.notify(val, 'info', null, null, onClick, timeout);
},
sign (number) {
return getSign(number);
@@ -66,14 +66,19 @@ export default {
round (number, nDigits) {
return round(number, nDigits);
},
notify (html, type, icon, sign) {
notify (html, type, icon, sign, onClick, timeout) {
if (typeof timeout === 'undefined') {
timeout = true;
}
this.$store.dispatch('snackbars:add', {
title: '',
text: html,
type,
icon,
sign,
timeout: true,
onClick,
timeout,
});
},
},

View File

@@ -176,6 +176,11 @@ export async function getGroupTasks (store, payload) {
return response.data.data;
}
export async function getCompletedGroupTasks (store, payload) {
let response = await axios.get(`/api/v4/tasks/group/${payload.groupId}?type=completedTodos`);
return response.data.data;
}
export async function createGroupTasks (store, payload) {
let response = await axios.post(`/api/v4/tasks/group/${payload.groupId}`, payload.tasks);
return response.data.data;

View File

@@ -1,4 +1,5 @@
{
"achievement": "Achievement",
"share": "Share",
"onwards": "Onwards!",
"levelup": "By accomplishing your real life goals, you leveled up and are now fully healed!",

View File

@@ -224,6 +224,6 @@
"level": "Level",
"allocated": "Allocated",
"buffs": "Buffs",
"pointsAvailable": "Points Available",
"statPoints": "Stat Points",
"pts": "pts"
}

View File

@@ -143,6 +143,7 @@
"PMDisabledCaptionText": "You can still send messages, but no one can send them to you.",
"block": "Block",
"unblock": "Un-block",
"blockWarning": "Block - This will have no effect if the player is a moderator now or becomes a moderator in future.",
"pm-reply": "Send a reply",
"inbox": "Inbox",
"messageRequired": "A message is required.",
@@ -475,5 +476,9 @@
"howToRequireApprovalDesc2": "Group Leaders and Managers can approve completed Tasks directly from the Task Board or from the Notifications panel.",
"whatIsGroupManager": "What is a Group Manager?",
"whatIsGroupManagerDesc": "A Group Manager is a user role that do not have access to the group's billing details, but can create, assign, and approve shared Tasks for the Group's members. Promote Group Managers from the Groups member list.",
"goToTaskBoard": "Go to Task Board"
"goToTaskBoard": "Go to Task Board",
"sharedCompletion": "Shared Completion",
"recurringCompletion": "None - Group task does not complete",
"singleCompletion": "Single - Completes when any assigned user finishes",
"allAssignedCompletion": "All - Completes when all assigned users finish"
}

View File

@@ -184,6 +184,7 @@
"mysticHourglassesTooltip": "Mystic Hourglasses",
"paypal": "PayPal",
"amazonPayments": "Amazon Payments",
"amazonPaymentsRecurring": "Ticking the checkbox below is necessary for your subscription to be created. It allows your Amazon account to be used for ongoing payments for <strong>this</strong> subscription. It will not cause your Amazon account to be automatically used for any future purchases.",
"timezone": "Time Zone",
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.",

View File

@@ -449,7 +449,7 @@ api.getGroupChallenges = {
method: 'GET',
url: '/challenges/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'party', 'guilds'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -463,10 +463,10 @@ api.getGroupChallenges = {
if (groupId === 'party') groupId = user.party._id;
if (groupId === 'habitrpg') groupId = TAVERN_ID;
let group = await Group.getGroup({user, groupId});
const group = await Group.getGroup({ user, groupId });
if (!group) throw new NotFound(res.t('groupNotFound'));
let challenges = await Challenge.find({group: groupId})
const challenges = await Challenge.find({ group: groupId })
.sort('-createdAt')
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
.exec();

View File

@@ -384,7 +384,7 @@ api.getGroup = {
method: 'GET',
url: '/groups/:groupId',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'party', 'guilds', 'contributor'],
})],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -5,6 +5,7 @@ import {
} from '../../libs/webhook';
import { removeFromArray } from '../../libs/collectionManipulators';
import * as Tasks from '../../models/task';
import { handleSharedCompletion } from '../../libs/groupTasks';
import { model as Challenge } from '../../models/challenge';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
@@ -287,7 +288,7 @@ api.getUserTasks = {
method: 'GET',
url: '/tasks/user',
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
userFieldsToInclude: ['_id', 'tasksOrder', 'preferences'],
})],
async handler (req, res) {
let types = Tasks.tasksTypes.map(type => `${type}s`);
@@ -297,10 +298,10 @@ api.getUserTasks = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let user = res.locals.user;
let dueDate = req.query.dueDate;
const user = res.locals.user;
const dueDate = req.query.dueDate;
let tasks = await getTasks(req, res, {user, dueDate});
const tasks = await getTasks(req, res, { user, dueDate });
return res.respond(200, tasks);
},
};
@@ -490,6 +491,9 @@ api.updateTask = {
if (sanitizedObj.requiresApproval) {
task.group.approval.required = true;
}
if (sanitizedObj.sharedCompletion) {
task.group.sharedCompletion = sanitizedObj.sharedCompletion;
}
setNextDue(task, user);
let savedTask = await task.save();
@@ -653,6 +657,12 @@ api.scoreTask = {
user.save(),
task.save(),
];
if (task.group && task.group.taskId) {
await handleSharedCompletion(task);
}
// Save results and handle request
if (taskOrderPromise) promises.push(taskOrderPromise);
let results = await Promise.all(promises);

View File

@@ -12,10 +12,13 @@ import {
getTasks,
moveTask,
} from '../../../libs/taskManager';
import { handleSharedCompletion } from '../../../libs/groupTasks';
import apiError from '../../../libs/apiError';
let requiredGroupFields = '_id leader tasksOrder name';
// @TODO: abstract to task lib
let types = Tasks.tasksTypes.map(type => `${type}s`);
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
function canNotEditTasks (group, user, assignedUserId) {
let isNotGroupLeader = group.leader !== user._id;
@@ -338,7 +341,7 @@ api.approveTask = {
}
// Remove old notifications
let managerPromises = [];
let approvalPromises = [];
managers.forEach((manager) => {
let notificationIndex = manager.notifications.findIndex(function findNotification (notification) {
return notification && notification.data && notification.data.taskId === task._id && notification.type === 'GROUP_TASK_APPROVAL';
@@ -346,7 +349,7 @@ api.approveTask = {
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
managerPromises.push(manager.save());
approvalPromises.push(manager.save());
}
});
@@ -362,9 +365,11 @@ api.approveTask = {
direction,
});
managerPromises.push(task.save());
managerPromises.push(assignedUser.save());
await Promise.all(managerPromises);
await handleSharedCompletion(task);
approvalPromises.push(task.save());
approvalPromises.push(assignedUser.save());
await Promise.all(approvalPromises);
res.respond(200, task);
},

View File

@@ -0,0 +1,61 @@
import * as Tasks from '../models/task';
const SHARED_COMPLETION = {
default: 'recurringCompletion',
single: 'singleCompletion',
every: 'allAssignedCompletion',
};
async function _completeMasterTask (masterTask) {
masterTask.completed = true;
await masterTask.save();
}
async function _deleteUnfinishedTasks (groupMemberTask) {
await Tasks.Task.deleteMany({
'group.taskId': groupMemberTask.group.taskId,
$and: [
{userId: {$exists: true}},
{userId: {$ne: groupMemberTask.userId}},
],
}).exec();
}
async function _evaluateAllAssignedCompletion (masterTask) {
let completions;
if (masterTask.group.approval && masterTask.group.approval.required) {
completions = await Tasks.Task.count({
'group.taskId': masterTask._id,
'group.approval.approved': true,
}).exec();
completions++;
} else {
completions = await Tasks.Task.count({
'group.taskId': masterTask._id,
completed: true,
}).exec();
}
if (completions >= masterTask.group.assignedUsers.length) {
await _completeMasterTask(masterTask);
}
}
async function handleSharedCompletion (groupMemberTask) {
let masterTask = await Tasks.Task.findOne({
_id: groupMemberTask.group.taskId,
}).exec();
if (!masterTask || !masterTask.group || masterTask.type !== 'todo') return;
if (masterTask.group.sharedCompletion === SHARED_COMPLETION.single) {
await _deleteUnfinishedTasks(groupMemberTask);
await _completeMasterTask(masterTask);
} else if (masterTask.group.sharedCompletion === SHARED_COMPLETION.every) {
await _evaluateAllAssignedCompletion(masterTask);
}
}
export {
SHARED_COMPLETION,
handleSharedCompletion,
};

View File

@@ -3,6 +3,9 @@ import * as Tasks from '../models/task';
import {
BadRequest,
} from './errors';
import {
SHARED_COMPLETION,
} from './groupTasks';
import _ from 'lodash';
import shared from '../../common';
@@ -96,6 +99,7 @@ export async function createTasks (req, res, options = {}) {
if (taskData.requiresApproval) {
newTask.group.approval.required = true;
}
newTask.group.sharedCompletion = taskData.sharedCompletion || SHARED_COMPLETION.default;
} else {
newTask.userId = user._id;
}
@@ -183,11 +187,12 @@ export async function getTasks (req, res, options = {}) {
limit = 0; // no limit
}
query = {
userId: user._id,
type: 'todo',
completed: true,
};
query.type = 'todo';
query.completed = true;
if (owner._id === user._id) {
query.userId = user._id;
}
sort = {
dateCompleted: -1,

View File

@@ -9,15 +9,19 @@ import url from 'url';
const COMMUNITY_MANAGER_EMAIL = nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL');
function getUserFields (userFieldsToExclude, req) {
function getUserFields (options, req) {
// A list of user fields that aren't needed for the route and are not loaded from the db.
// Must be an array
if (userFieldsToExclude) {
return userFieldsToExclude.map(field => {
if (options.userFieldsToExclude) {
return options.userFieldsToExclude.map(field => {
return `-${field}`; // -${field} means exclude ${field} in mongodb
}).join(' ');
}
if (options.userFieldsToInclude) {
return options.userFieldsToInclude.join(' ');
}
// Allows GET requests to /user to specify a list of user fields to return instead of the entire doc
// Notifications are always included
const urlPath = url.parse(req.url).pathname;
@@ -50,7 +54,7 @@ export function authWithHeaders (options = {}) {
apiToken,
};
const fields = getUserFields(options.userFieldsToExclude, req);
const fields = getUserFields(options, req);
const findPromise = fields ? User.findOne(userQuery).select(fields) : User.findOne(userQuery);
return findPromise

View File

@@ -1319,6 +1319,7 @@ schema.methods.updateTask = async function updateTask (taskToSync, options = {})
updateCmd.$set['group.approval.required'] = taskToSync.group.approval.required;
updateCmd.$set['group.assignedUsers'] = taskToSync.group.assignedUsers;
updateCmd.$set['group.sharedCompletion'] = taskToSync.group.sharedCompletion;
let taskSchema = Tasks[taskToSync.type];
@@ -1414,6 +1415,7 @@ schema.methods.syncTask = async function groupSyncTask (taskToSync, user) {
matchingTask.group.approval.required = taskToSync.group.approval.required;
matchingTask.group.assignedUsers = taskToSync.group.assignedUsers;
matchingTask.group.sharedCompletion = taskToSync.group.sharedCompletion;
// sync checklist
if (taskToSync.checklist) {

View File

@@ -6,6 +6,7 @@ import baseModel from '../libs/baseModel';
import { InternalServerError } from '../libs/errors';
import _ from 'lodash';
import { preenHistory } from '../libs/preening';
import { SHARED_COMPLETION } from '../libs/groupTasks';
const Schema = mongoose.Schema;
@@ -111,6 +112,7 @@ export let TaskSchema = new Schema({
requested: {type: Boolean, default: false},
requestedDate: {type: Date},
},
sharedCompletion: {type: String, enum: _.values(SHARED_COMPLETION), default: SHARED_COMPLETION.default},
},
reminders: [{