Skip to content
Commits on Source (44)
......@@ -39,7 +39,7 @@ stages:
- !reference [.rules-map, on-dev]
- !reference [.rules-map, not-on-semantic-release-commit]
- !reference [.rules-map, on-branch]
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.13
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.13.3
cache:
key:
files:
......@@ -71,7 +71,7 @@ cache-dependencies:
# This job update dependencies
policy: pull-push
script:
- meteor npm ci
- meteor npm install
###############################################################################
# `test` stage: `meteor-lint`, `meteor-tests`
......
......@@ -33,7 +33,7 @@ eoleteam:accounts-keycloak@2.1.0
eoleteam:keycloak-oauth@2.2.0
es5-shim@4.8.0
fetch@0.1.3
fourseven:scss@4.15.0
fourseven:scss@4.16.0
geojson-utils@1.0.11
hot-code-push@1.0.4
hot-module-replacement@0.5.3
......@@ -49,7 +49,7 @@ logging@1.3.2
mdg:validated-method@1.3.0
meteor@1.11.3
meteor-base@1.5.1
meteortesting:browser-tests@1.5.1
meteortesting:browser-tests@1.5.3
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mexar:mdt@0.2.2
......
# The tag here should match the Meteor version of your app, per .meteor/release
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.13
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.13.3
# Copy app package.json and package-lock.json into container
#COPY ./app/package*.json $APP_SOURCE_FOLDER/
......
......@@ -50,6 +50,11 @@ const settingsParticipant = new SimpleSchema({
EventsAgenda.schema = new SimpleSchema(
{
eventType: {
type: String,
optional: true,
defaultValue: 'rdv',
},
title: {
type: String,
optional: false,
......@@ -130,6 +135,7 @@ EventsAgenda.publicFields = {
title: 1,
location: 1,
description: 1,
eventType: 1,
start: 1,
end: 1,
allDay: 1,
......
......@@ -12,20 +12,18 @@ export const meetingTemplate = ({ sender, date }) => `
<br/>
`;
export const meetingCancelTemplate = ({ date, content }) => `
<h4>Votre rendez vous le ${date} a été annulé</h4>
export const meetingCancelTemplate = ({ date, meetWith, content }) => `
<h4>Votre rendez vous le ${date} avec ${meetWith} a été annulé</h4>
<br/>
<div>
${content}
</div>
`;
export const meetingEditTemplate = ({ date, email, name }) => `
export const meetingEditTemplate = ({ email, name }) => `
<h4>Votre rendez vous a été édité</h4>
<br/>
<div>
Date: ${date}
<br/>
Votre nom: ${name}
<br/>
Votre adresse email: ${email}
......
......@@ -39,13 +39,18 @@ describe('events', function () {
anotherUser = Factory.create('user');
ownerPollUser = Factory.create('user');
poll = Factory.create('poll', { active: false, public: true, userId: ownerPollUser._id });
pollAnswer = Factory.create('poll_answer', { userId: anotherUser._id, email: anotherUser.emails[0].address });
pollAnswer = Factory.create('poll_answer', {
userId: anotherUser._id,
email: anotherUser.emails[0].address,
meetingSlot: [new Date()],
name: `${anotherUser.firstName} ${anotherUser.lastName}`,
});
});
describe('createEventAgendaMeeting', function () {
it('should create an event meeting into agenda with a connected user', function () {
createEventAgendaMeeting(poll, pollAnswer, ownerPollUser._id);
const resultEvent = EventsAgenda.findOne({ title: poll.title });
const resultEvent = EventsAgenda.findOne({ title: `${poll.title} (${pollAnswer.name})` });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
assert.typeOf(resultEvent.end, 'date');
......@@ -54,9 +59,13 @@ describe('events', function () {
assert.isEmpty(resultEvent.guests);
});
it('should create an event meeting into agenda with a guest user', function () {
const anotherPollAnswer = Factory.create('poll_answer', { email: 'toto@test.com' });
const anotherPollAnswer = Factory.create('poll_answer', {
email: 'toto@test.com',
meetingSlot: [new Date()],
name: 'toto test',
});
createEventAgendaMeeting(poll, anotherPollAnswer, ownerPollUser._id);
const resultEvent = EventsAgenda.findOne({ title: poll.title });
const resultEvent = EventsAgenda.findOne({ title: `${poll.title} (${anotherPollAnswer.name})` });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
assert.typeOf(resultEvent.end, 'date');
......
......@@ -14,33 +14,36 @@ import {
import { EventsAgenda } from '../events';
export function sendEmail(poll, answer) {
const cal = ical({ domain: process.env.ROOT_URL, name: 'sondage iCal' });
cal.createEvent({
start: moment(answer.meetingSlot),
end: moment(answer.meetingSlot).add(DURATIONS_TIME[poll.duration], 'minute'),
summary: poll.title,
description: poll.description,
url: new URL(ROUTES.ANSWER_POLL_RM(poll._id), process.env.ROOT_URL).href,
});
const template = poll.type === POLLS_TYPES.POLL ? eventTemplate : meetingTemplate;
const html = template({
title: poll.title,
sender: Meteor.users.findOne(poll.userId),
date: moment(answer.meetingSlot).format('LLL (Z)'),
});
try {
Email.send({
to: answer.email,
from: Meteor.settings.smtp.fromEmail,
subject: `Sondage - Votre rdv du ${moment(answer.meetingSlot).format('L')}`,
icalEvent: cal.toString(),
inReplyTo: Meteor.settings.smtp.toEmail,
html,
const slots = !Array.isArray(answer.meetingSlot) ? [answer.meetingSlot] : answer.meetingSlot;
slots.forEach((slot) => {
const cal = ical({ domain: process.env.ROOT_URL, name: 'sondage iCal' });
cal.createEvent({
start: moment(slot),
end: moment(slot).add(DURATIONS_TIME[poll.duration], 'minute'),
summary: poll.title,
description: poll.description,
url: new URL(ROUTES.ANSWER_POLL_RM(poll._id), process.env.ROOT_URL).href,
});
} catch (error) {
console.log(error);
throw new Meteor.Error('api.events.methods.sendEmail', 'api.errors.cannotSendEmail');
}
const template = poll.type === POLLS_TYPES.POLL ? eventTemplate : meetingTemplate;
const html = template({
title: poll.title,
sender: Meteor.users.findOne(poll.userId),
date: moment(slot).format('LLL (Z)'),
});
try {
Email.send({
to: answer.email,
from: Meteor.settings.smtp.fromEmail,
subject: `Sondage - Votre rdv du ${moment(slot).format('L')}`,
icalEvent: cal.toString(),
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
} catch (error) {
console.log(error);
throw new Meteor.Error('api.events.methods.sendEmail', 'api.errors.cannotSendEmail');
}
});
}
export function sendEmailToCreator(poll, answer, userId) {
......@@ -54,49 +57,71 @@ export function sendEmailToCreator(poll, answer, userId) {
}
const admin = Meteor.users.findOne(poll.userId);
const html = template({
title: poll.title,
sender: answerer,
date: moment(answer.meetingSlot).format('LLL (Z)'),
url: `${Meteor.settings.public.services.sondagesUrl}/poll/answer/${poll._id} `,
connected,
const slots = !Array.isArray(answer.meetingSlot) ? [answer.meetingSlot] : answer.meetingSlot;
slots.forEach((slot) => {
const html = template({
title: poll.title,
sender: answerer,
date: moment(slot).format('LLL (Z)'),
url: `${Meteor.settings.public.services.sondagesUrl}/poll/answer/${poll._id} `,
connected,
});
try {
Email.send({
to: admin.emails[0].address,
from: Meteor.settings.smtp.fromEmail,
subject: `Rendez-vous - nouveau créneau sélectionné pour le rendez-vous ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
} catch (error) {
console.log(error);
throw new Meteor.Error('api.events.methods.sendEmailToCreator', 'api.errors.cannotSendEmailToCreator');
}
});
}
function _sendCancelEmail(email, poll, slot, meetWith, content) {
const template = meetingCancelTemplate;
const html = template({ date: moment(slot).format('LLL (Z)'), meetWith, content });
try {
Email.send({
to: admin.emails[0].address,
to: email,
from: Meteor.settings.smtp.fromEmail,
subject: `Rendez-vous - nouveau créneau sélectionné pour le rendez-vous ${poll.title}`,
subject: `Sondage - annulation de votre rendez-vous pour ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
} catch (error) {
console.log(error);
throw new Meteor.Error('api.events.methods.sendEmailToCreator', 'api.errors.cannotSendEmailToCreator');
throw new Meteor.Error('api.events.methods.sendCancelEmail', 'api.errors.cannotSendEmail');
}
}
export function sendCancelEmail(poll, answer, content) {
const template = meetingCancelTemplate;
const pollOwner = Meteor.users.findOne(poll.userId);
const ownerEmail = pollOwner?.emails[0].address;
const meetWith = `${pollOwner.firstName} ${pollOwner.lastName} (${ownerEmail})`;
answer.meetingSlot.forEach((slot) => {
_sendCancelEmail(answer.email, poll, slot, meetWith, content);
});
}
const html = template({ date: moment(answer.meetingSlot).format('LLL (Z)'), content });
try {
Email.send({
to: answer.email,
from: Meteor.settings.smtp.fromEmail,
subject: `Sondage - annulation de votre rendez-vous pour ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
export function sendCancelEmailToCreator(poll, answer, content) {
const pollOwner = Meteor.users.findOne(poll.userId);
const ownerEmail = pollOwner?.emails[0].address;
if (ownerEmail) {
const meetWith = `${answer.name} (${answer.email})`;
answer.meetingSlot.forEach((slot) => {
_sendCancelEmail(ownerEmail, poll, slot, meetWith, content);
});
} catch (error) {
console.log(error);
throw new Meteor.Error('api.events.methods.sendCancelEmail', 'api.errors.cannotSendEmail');
}
}
export function sendEditEmail(poll, email, name, meetingSlot) {
export function sendEditEmail(poll, email, name) {
const template = meetingEditTemplate;
const html = template({ date: moment(meetingSlot).format('LLL (Z)'), email, name });
const html = template({ email, name });
try {
Email.send({
to: email,
......@@ -113,27 +138,48 @@ export function sendEditEmail(poll, email, name, meetingSlot) {
export function createEventAgendaMeeting(poll, answer, userId) {
const participantUser = Accounts.findUserByEmail(answer.email);
EventsAgenda.insert({
title: poll.title,
location: '',
start: moment(answer.meetingSlot).format(),
end: moment(answer.meetingSlot).add(DURATIONS_TIME[poll.duration], 'minute').format(),
allDay: poll.allDay,
participants: participantUser
? [
{
_id: participantUser._id,
email: answer.email,
},
]
: [],
guests: participantUser ? [] : [answer.email],
description: poll.description,
groups: [],
userId,
const slots = !Array.isArray(answer.meetingSlot) ? [answer.meetingSlot] : answer.meetingSlot;
slots.forEach((slot) => {
const title = `${poll.title} (${answer.name})`;
const description = `Rendez vous avec ${answer.name}`;
EventsAgenda.insert({
title,
location: '',
start: moment(slot).format(),
end: moment(slot).add(DURATIONS_TIME[poll.duration], 'minute').format(),
allDay: poll.allDay,
participants: participantUser
? [
{
_id: participantUser._id,
email: answer.email,
},
]
: [],
guests: participantUser ? [] : [answer.email],
description,
groups: [],
userId,
});
});
}
export function deleteEventAgendaMeeting(poll, answer, userId) {
// events have been created only if answer is confirmed
if (answer.confirmed) {
const title = `${poll.title} (${answer.name})`;
const slots = !Array.isArray(answer.meetingSlot) ? [answer.meetingSlot] : answer.meetingSlot;
slots.forEach((slot) => {
EventsAgenda.remove({
title,
start: slot,
allDay: poll.allDay,
userId,
});
});
}
}
export function createEventAgenda(poll, date, userId) {
let answers = [];
if (poll.public) {
......
......@@ -144,6 +144,11 @@ Polls.schema = new SimpleSchema(
return new Date();
},
},
hideParticipantsList: {
type: Boolean,
label: 'Hide participants list',
defaultValue: false,
},
},
{ clean: { removeEmptyStrings: false }, tracker: Tracker },
);
......
import { Random } from 'meteor/random';
import { Factory } from 'meteor/dburles:factory';
import faker from 'faker';
import { faker } from '@faker-js/faker';
import Polls from '../polls';
Factory.define('poll', Polls, {
......
......@@ -108,15 +108,6 @@ export const validatePollAnswer = new ValidatedMethod({
} else if (poll.completed) {
throw new Meteor.Error('api.polls_answers.methods.validate.notAllowed', 'api.errors.notAllowed');
}
if (poll.public) {
const answers = PollsAnswers.find({ userId: null, pollId }).fetch();
answers.forEach((answer) =>
sendEmail(poll, {
...answer,
meetingSlot: date,
}),
);
}
if (poll.groups.length) {
createEventAgenda(poll, date, this.userId);
......@@ -132,7 +123,23 @@ export const validatePollAnswer = new ValidatedMethod({
});
}
}
return Polls.update({ _id: pollId }, { $set: { completed: true, choosenDate: date, active: false } });
const result = Polls.update({ _id: pollId }, { $set: { completed: true, choosenDate: date, active: false } });
if (poll.public) {
const answers = PollsAnswers.find({ userId: null, pollId }).fetch();
let emailErrors = false;
answers.forEach((answer) => {
try {
sendEmail(poll, {
...answer,
meetingSlot: date,
});
} catch (error) {
emailErrors = true;
}
});
if (emailErrors) throw new Meteor.Error('api.events.methods.sendEmail', 'api.errors.cannotSendEmail');
}
return result;
},
});
......
......@@ -66,9 +66,12 @@ PollsAnswers.schema = new SimpleSchema(
label: 'Poll ID',
},
meetingSlot: {
type: Date,
type: Array,
label: 'Meeting date slot',
optional: true,
defaultValue: [],
},
'meetingSlot.$': {
type: Date,
},
choices: {
type: Array,
......
......@@ -9,12 +9,15 @@ import Groups from '../../groups/groups';
import Polls from '../../polls/polls';
import {
createEventAgendaMeeting,
deleteEventAgendaMeeting,
sendEmail,
sendCancelEmail,
sendCancelEmailToCreator,
sendEditEmail,
sendEmailToCreator,
} from '../../events/server/methods';
import validateString from '../../../utils/functions/strings';
import slotsIncludes from '../../../utils/functions/answers';
export const createPollAnswers = new ValidatedMethod({
name: 'polls_answers.create',
......@@ -25,13 +28,9 @@ export const createPollAnswers = new ValidatedMethod({
run({ data }) {
const poll = Polls.findOne({ _id: data.pollId });
if (PollsAnswers.findOne({ pollId: data.pollId, email: data.email }) && !this.userId) {
const previousAnswer = PollsAnswers.findOne({ pollId: data.pollId, email: data.email });
if (previousAnswer && !this.userId) {
throw new Meteor.Error('api.polls_answers.methods.create.emailAlreadyVoted', 'api.errors.emailAlreadyVoted');
} else if (
poll.type === POLLS_TYPES.MEETING &&
PollsAnswers.findOne({ pollId: data.pollId, meetingSlot: data.meetingSlot })
) {
throw new Meteor.Error('api.polls_answers.methods.create.slotAlreadyTaken', 'api.errors.slotAlreadyTaken');
} else if (poll.type === POLLS_TYPES.MEETING && poll.userId === this.userId) {
throw new Meteor.Error(
'api.polls_answers.methods.create.youCantHaveAMeetingWithYourself',
......@@ -39,6 +38,12 @@ export const createPollAnswers = new ValidatedMethod({
);
} else if (poll.completed) {
throw new Meteor.Error('api.polls_answers.methods.create.notAllowed', 'api.errors.notAllowed');
} else if (poll.type === POLLS_TYPES.MEETING) {
// check if any meeting slot is already taken
data.meetingSlot.forEach((slot) => {
if (PollsAnswers.findOne({ pollId: data.pollId, userId: { $ne: this.userId }, meetingSlot: slot }))
throw new Meteor.Error('api.polls_answers.methods.create.slotAlreadyTaken', 'api.errors.slotAlreadyTaken');
});
}
validateString(data.email);
validateString(data.name);
......@@ -74,12 +79,30 @@ export const createPollAnswers = new ValidatedMethod({
throw new Meteor.Error('api.polls_answers.methods.create.notAllowed', 'api.errors.notAllowed');
}
} else if ((poll.public || this.userId) && poll.active) {
if (poll.type === POLLS_TYPES.MEETING) sendEmailToCreator(poll, data, this.userId);
return PollsAnswers.update(
const result = PollsAnswers.update(
{ pollId: data.pollId, email: data.email },
{ $set: { ...data, userId: this.userId, confirmed: false } },
{ upsert: true },
);
if (poll.type === POLLS_TYPES.MEETING) {
// email poll owner about new slots chosen by user
sendEmailToCreator(poll, data, this.userId);
if (previousAnswer && previousAnswer.confirmed) {
// if previous answer was confirmed by poll owner
// - delete previously created events from poll owner agenda
// - email poll owner about cancelled meeting slots
deleteEventAgendaMeeting(poll, previousAnswer, poll.userId);
const initialSlots = Array.isArray(previousAnswer.meetingSlot)
? previousAnswer.meetingSlot
: [previousAnswer.meetingSlot];
initialSlots.forEach((slot) => {
if (!slotsIncludes(data.meetingSlot, slot)) {
sendCancelEmailToCreator(poll, { ...data, meetingSlot: [slot] }, '');
}
});
}
}
return result;
} else if (!poll.public && !this.userId) {
throw new Meteor.Error('api.polls_answers.methods.create.notPublic', 'api.errors.pollNotActive');
} else {
......@@ -106,11 +129,10 @@ export const validateMeetingPollAnswer = new ValidatedMethod({
} else if (poll.userId !== this.userId) {
throw new Meteor.Error('api.polls_answers.methods.validate.notAllowed', 'api.errors.notAllowed');
}
sendEmail(poll, answer);
const result = PollsAnswers.update({ _id: answerId }, { $set: { confirmed: true } });
createEventAgendaMeeting(poll, answer, this.userId);
return PollsAnswers.update({ _id: answerId }, { $set: { confirmed: true } });
sendEmail(poll, answer);
return result;
},
});
......@@ -131,8 +153,10 @@ export const cancelMeetingPollAnswer = new ValidatedMethod({
if (poll.userId !== this.userId || poll.type !== POLLS_TYPES.MEETING) {
throw new Meteor.Error('api.polls_answers.methods.cancel.notAllowed', 'api.errors.notAllowed');
}
const result = PollsAnswers.remove({ _id: answerId });
if (emailNotice) sendCancelEmail(poll, answer, emailContent);
return PollsAnswers.remove({ _id: answerId });
deleteEventAgendaMeeting(poll, answer, this.userId);
return result;
},
});
......@@ -146,10 +170,17 @@ export const editMeetingPollAnswer = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Email,
},
name: String,
meetingSlot: Date,
meetingSlot: Array,
'meetingSlot.$': {
type: Date,
},
initialSlots: Array,
'initialSlots.$': {
type: Date,
},
}).validator(),
run({ answerId, emailNotice, email, name, meetingSlot }) {
run({ answerId, emailNotice, email, name, meetingSlot, initialSlots }) {
const answer = PollsAnswers.findOne({ _id: answerId });
if (!answer) {
throw new Meteor.Error('api.polls_answers.methods.edit.notFound', 'api.errors.answerNotFound');
......@@ -160,8 +191,26 @@ export const editMeetingPollAnswer = new ValidatedMethod({
}
validateString(email);
validateString(name);
if (emailNotice) sendEditEmail(poll, email, name, meetingSlot);
return PollsAnswers.update({ _id: answerId }, { $set: { email, name, meetingSlot } });
const result = PollsAnswers.update({ _id: answerId }, { $set: { email, name, meetingSlot } });
if (emailNotice) {
// email user if user information has changed
if (answer.email !== email || answer.name !== name) sendEditEmail(poll, email, name);
// email user if new meeting created
meetingSlot.forEach((slot) => {
if (answer.confirmed && !slotsIncludes(initialSlots, slot)) {
createEventAgendaMeeting(poll, { ...answer, name, email, meetingSlot: [slot] }, this.userId);
sendEmail(poll, { ...answer, name, email, meetingSlot: [slot] });
}
});
// email user if meeting cancelled
initialSlots.forEach((slot) => {
if (!slotsIncludes(meetingSlot, slot)) {
deleteEventAgendaMeeting(poll, { ...answer, name, email, meetingSlot: [slot] }, this.userId);
sendCancelEmail(poll, { ...answer, name, email, meetingSlot: [slot] }, '');
}
});
}
return result;
},
});
......
......@@ -3,7 +3,7 @@ import { assert } from 'chai';
import { Meteor } from 'meteor/meteor';
import { _ } from 'meteor/underscore';
import { Random } from 'meteor/random';
import faker from 'faker';
import { faker } from '@faker-js/faker';
import { Factory } from 'meteor/dburles:factory';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import { POLLS_TYPES } from '../../../utils/enums';
......@@ -53,6 +53,7 @@ describe('polls_answers', function () {
let ownerPollUser;
let poll;
let pollTypePoll;
let pollTypePollHideParticipants;
let meetingPoll;
beforeEach(function () {
......@@ -80,6 +81,19 @@ describe('polls_answers', function () {
email: ownerPollUser.emails[0].address,
userId: ownerPollUser._id,
});
// variables for poll type poll with hide participants
pollTypePollHideParticipants = Factory.create('poll', {
active: true,
userId: ownerPollUser._id,
type: 'POLL',
hideParticipantsList: true,
});
_.times(7, () => {
Factory.create('poll_answer', {
pollId: pollTypePollHideParticipants._id,
email: anotherUser.emails[0].address,
});
});
// variables for poll type meeting
meetingPoll = Factory.create('poll', { active: true, userId: ownerPollUser._id, type: 'MEETING' });
_.times(3, () => {
......@@ -122,6 +136,14 @@ describe('polls_answers', function () {
done();
});
});
it('does return all pollanswers exclude name and email', function (done) {
const collector = new PublicationCollector({});
collector.collect('polls_answers.onePoll', { pollId: pollTypePollHideParticipants._id }, (collections) => {
assert.equal(collections.counts[0].count, 7);
assert.doesNotHaveAnyKeys(collections.polls_answers[0], ['name', 'email']);
done();
});
});
it('does return all pollanswers include our own pollAnswer without user account ', function (done) {
const collector = new PublicationCollector({});
collector.collect('polls_answers.onePoll', { pollId: pollTypePoll._id }, (collections) => {
......@@ -225,6 +247,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......@@ -241,6 +264,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: false,
pollId: poll._id,
});
......@@ -257,18 +281,22 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
pollId: poll._id,
meetingSlot: [new Date()],
});
validateMeetingPollAnswer._execute({ userId: ownerPollUser._id }, { answerId: pollAnswer._id });
const eventResult = EventsAgenda.findOne({ title: poll.title });
assert.equal(eventResult.title, 'Pour le test');
const eventResult = EventsAgenda.findOne({ title: `${poll.title} (${pollAnswer.name})` });
assert.equal(eventResult.title, `Pour le test (${pollAnswer.name})`);
});
it('should update pollAnswer', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id });
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
pollId: poll._id,
meetingSlot: [new Date()],
});
validateMeetingPollAnswer._execute({ userId: ownerPollUser._id }, { answerId: pollAnswer._id });
const resultPollAnswer = PollsAnswers.findOne({ _id: pollAnswer._id });
......@@ -286,6 +314,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......@@ -305,6 +334,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......@@ -324,12 +354,14 @@ describe('polls_answers', function () {
public: true,
type: POLLS_TYPES.MEETING,
});
const slot = new Date();
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: 'toto',
confirmed: true,
pollId: poll._id,
meetingSlot: [slot],
});
assert.throws(
() => {
......@@ -340,7 +372,8 @@ describe('polls_answers', function () {
emailNotice: false,
email: 'newmail@test.fr',
name: 'titi',
meetingSlot: new Date(),
meetingSlot: [slot],
initialSlots: [slot],
},
);
},
......@@ -357,6 +390,7 @@ describe('polls_answers', function () {
name: 'toto',
confirmed: true,
pollId: poll._id,
meetingSlot: [],
});
editMeetingPollAnswer._execute(
{ userId: ownerPollUser._id },
......@@ -365,13 +399,14 @@ describe('polls_answers', function () {
emailNotice: false,
email: 'newmail@test.fr',
name: 'titi',
meetingSlot: newSlot,
meetingSlot: [newSlot],
initialSlots: [],
},
);
const resultPollAnswer = PollsAnswers.findOne({ _id: pollAnswer._id });
assert.equal(resultPollAnswer.name, 'titi');
assert.equal(resultPollAnswer.email, 'newmail@test.fr');
assert.equal(resultPollAnswer.meetingSlot.toString(), newSlot.toString());
assert.equal(resultPollAnswer.meetingSlot[0].toString(), newSlot.toString());
});
});
describe('getPollanswer', function () {
......@@ -380,6 +415,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......@@ -396,6 +432,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......@@ -412,6 +449,7 @@ describe('polls_answers', function () {
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: `${ownerPollUser.firstName} ${ownerPollUser.lastName}`,
confirmed: true,
pollId: poll._id,
});
......
......@@ -14,11 +14,19 @@ Meteor.publish('polls_answers.getCurrentUser', function pollAnswersCurrentUser({
Meteor.publish('polls_answers.onePoll', function pollAnswersOne({ pollId }) {
let pollOwner = false;
const poll = Polls.findOne(pollId);
if (poll && poll.userId === this.userId) pollOwner = true;
const query = { pollId };
if (this.userId) {
query.userId = { $ne: this.userId };
if (poll) {
if (poll.userId === this.userId) pollOwner = true;
const query = { pollId };
if (this.userId) {
query.userId = { $ne: this.userId };
}
Counts.publish(this, 'polls_answers.onePoll', PollsAnswers.find(query), { noReady: true });
return PollsAnswers.find(
query,
pollOwner || (poll.type === POLLS_TYPES.POLL && !poll.hideParticipantsList)
? {}
: { fields: { email: 0, name: 0 } },
);
}
Counts.publish(this, 'polls_answers.onePoll', PollsAnswers.find(query), { noReady: true });
return PollsAnswers.find(query, pollOwner || poll.type === POLLS_TYPES.POLL ? {} : { fields: { email: 0, name: 0 } });
return this.ready();
});
import { Factory } from 'meteor/dburles:factory';
import faker from 'faker';
import { faker } from '@faker-js/faker';
Factory.define('user', Meteor.users, {
firstName: () => faker.name.firstName(),
lastName: () => faker.name.lastName(),
firstName: () => faker.person.firstName(),
lastName: () => faker.person.lastName(),
emails: () => [{ address: faker.internet.email() }],
});
<script>
import { globalState } from "/imports/utils/functions/stores";
import { globalState } from '/imports/utils/functions/stores';
const { state } = globalState();
export let link = "",
text = "",
export let loading = false;
export let link = '',
text = '',
action = null,
color = "is-primary",
color = 'is-primary',
disabled = false;
const onClick = () => (action ? action() : null);
</script>
......@@ -13,6 +14,7 @@
{#if action && !link}
<button
class:is-fullwidth={$state.mobile}
class:is-loading={loading}
class:disabled
class={`button ${color}`}
on:click={disabled ? null : onClick}
......@@ -25,7 +27,7 @@
class:disabled
class={`button ${color}`}
rel="prefetch"
href={disabled ? "" : link}
href={disabled ? '' : link}
on:click={disabled ? null : onClick}
>
{text}</a
......@@ -37,8 +39,10 @@
button {
font-size: 16px;
transition: all 0.2s ease-in-out;
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
box-shadow:
0px 3px 1px -2px rgba(0, 0, 0, 0.2),
0px 2px 2px 0px rgba(0, 0, 0, 0.14),
0px 1px 5px 0px rgba(0, 0, 0, 0.12);
border-radius: 8px;
}
.disabled {
......
<script>
import { fade } from "svelte/transition";
export let message;
import { fade } from 'svelte/transition';
export let message = null;
export let mainLoader = false;
const cubes = [
"0.5s",
"0.6s",
"0.7s",
"0.8s",
"0.9s",
"0.4s",
"0.5s",
"0.6s",
"0.7s",
"0.8s",
"0.3s",
"0.4s",
"0.5s",
"0.6s",
"0.7s",
"0.2s",
"0.3s",
"0.4s",
"0.5s",
"0.6s",
"0.1s",
"0.2s",
"0.3s",
"0.4s",
"0.5s",
'0.5s',
'0.6s',
'0.7s',
'0.8s',
'0.9s',
'0.4s',
'0.5s',
'0.6s',
'0.7s',
'0.8s',
'0.3s',
'0.4s',
'0.5s',
'0.6s',
'0.7s',
'0.2s',
'0.3s',
'0.4s',
'0.5s',
'0.6s',
'0.1s',
'0.2s',
'0.3s',
'0.4s',
'0.5s',
];
</script>
<div class:mainLoader class="wrapper" transition:fade|local={{ duration: 200, delay: 200 }}>
<div class="loader-wrapper">
<div class="sk-grid">
{#each cubes as cube, i}
<div class="sk-grid-cube" style="--time:{cube}; --position:{(i % 5) * 25}% {Math.floor(i / 5) * 25}%" />
{/each}
</div>
{#if message}<span class="subtitle">{message}</span>{/if}
</div>
</div>
<style>
.wrapper {
width: 100%;
......@@ -76,7 +88,7 @@
float: left;
animation: sk-grid 1.5s infinite ease-in-out;
animation-delay: var(--time);
background-image: url("/puce_eole.png");
background-image: url('/puce_eole.png');
background-repeat: no-repeat;
background-attachment: inherit;
background-position: var(--position);
......@@ -94,20 +106,3 @@
}
}
</style>
<div
class:mainLoader
class="wrapper"
transition:fade|local={{ duration: 200, delay: 200 }}>
<div class="loader-wrapper">
<div class="sk-grid">
{#each cubes as cube, i}
<div
class="sk-grid-cube"
style="--time:{cube}; --position:{(i % 5) * 25}% {Math.floor(i / 5) * 25}%" />
{/each}
</div>
{#if message}<span class="subtitle">{message}</span>{/if}
</div>
</div>
......@@ -66,12 +66,8 @@
</button>
{/if}
<a href={!poll.active && $votes === 0 ? ROUTES.EDIT_POLL_RM(poll._id) : ROUTES.ADMIN}>
<button
class="button is-small is-light"
disabled={poll.active || $votes !== 0}
title={$_('pages.home.edit_tooltip')}
>
<a href={!poll.active ? ROUTES.EDIT_POLL_RM(poll._id) : ROUTES.ADMIN}>
<button class="button is-small is-light" disabled={poll.active} title={$_('pages.home.edit_tooltip')}>
<i class="fas fa-pen" />
</button>
</a>
......
......@@ -8,11 +8,12 @@
import timeGridPlugin from '@fullcalendar/timegrid';
import listView from '@fullcalendar/list';
import PollsAnswers from '../../../api/polls_answers/polls_answers';
import slotsIncludes from '../../../utils/functions/answers';
export let answer = {};
export let poll = {};
export let toggleChoice = () => null;
export let currentAnswer = '';
export let currentAnswer = {};
export let editMode = false;
let answers;
......@@ -20,9 +21,15 @@
let events;
$: answers = useTracker(() => {
// load all answers for this poll, except current user's answer
Meteor.subscribe('polls_answers.onePoll', { pollId: poll._id });
Meteor.subscribe('polls_answers.getCurrentUser', { pollId: poll._id });
return PollsAnswers.find({ pollId: poll._id }).fetch();
return PollsAnswers.find({ pollId: poll._id })
.fetch()
.map((a) => {
// change meetingSlot to array for old pollAnswers
a.meetingSlot = Array.isArray(a.meetingSlot) ? a.meetingSlot : [a.meetingSlot];
return a;
});
});
function canSeeEmail(myemail, email) {
......@@ -33,18 +40,19 @@
if (Meteor.userId() === poll.userId && !editMode) {
return;
}
if (!$answers.find((a) => moment(a.meetingSlot).isSame(event.start))) {
if (moment(answer.meetingSlot).isSame(event.start)) {
toggleChoice(null);
} else {
toggleChoice(event.start);
}
if (!$answers.find((a) => slotsIncludes(a.meetingSlot, event.start) && a._id !== answer._id)) {
toggleChoice(event.start);
}
};
$: events = poll.meetingSlots.map(({ start, end }) => {
const answerToSlot = $answers.find((a) => moment(a.meetingSlot).isSame(start));
if (moment(answer.meetingSlot).isSame(start)) {
const answerToSlot = slotsIncludes(answer.meetingSlot, start)
? answer
: $answers.find((a) => {
if (editMode && a.email === answer.email) return false;
return slotsIncludes(a.meetingSlot, start);
});
if (slotsIncludes(answer.meetingSlot, start)) {
return {
start,
end,
......@@ -64,8 +72,8 @@
className: !canSeeEmail(currentAnswer.email, answerToSlot.email)
? 'fc-slot-unavailable'
: answerToSlot.confirmed
? 'fc-slot-confirmed'
: 'fc-slot-taken',
? 'fc-slot-confirmed'
: 'fc-slot-taken',
};
} else {
return {
......@@ -90,11 +98,11 @@
},
buttonText: {
today: $_('today'),
listWeek: $_('pages.answer.listWeek'),
listYear: $_('pages.answer.listYear'),
timeGridWeek: $_('pages.answer.timeGridWeek'),
},
headerToolbar: {
left: 'timeGridWeek listWeek',
left: 'timeGridWeek listYear',
},
locale: $locale,
timeZone: 'local',
......