Skip to content
Commits on Source (26)
......@@ -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.8.1
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.11.0
cache:
key:
files:
......
......@@ -6,22 +6,22 @@
meteor-base@1.5.1 # Packages every Meteor app needs to have
mobile-experience@1.1.0 # Packages for a great mobile UX
mongo@1.16.1 # The database Meteor supports right now
mongo@1.16.5 # The database Meteor supports right now
reactive-var@1.0.12 # Reactive variable for tracker
tracker@1.2.1 # Meteor's client-side reactive programming library
tracker@1.3.1 # Meteor's client-side reactive programming library
fourseven:scss
standard-minifier-js@2.8.1 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
ecmascript@0.16.3 # Enable ECMAScript2015+ syntax in app code
typescript@4.5.4 # Enable TypeScript syntax in .ts and .tsx modules
ecmascript@0.16.6 # Enable ECMAScript2015+ syntax in app code
typescript@4.9.4 # Enable TypeScript syntax in .ts and .tsx modules
shell-server@0.5.0 # Server-side component of the `meteor shell` command
zodern:melte
rdb:svelte-meteor-data
static-html@1.3.2
accounts-password@2.3.1
accounts-base@2.2.5
accounts-password@2.3.4
accounts-base@2.2.7
hot-module-replacement@0.5.2
aldeed:collection2
aldeed:schema-index
......@@ -30,7 +30,7 @@ mdg:validated-method
mexar:mdt
ddp-rate-limiter@1.1.1
tmeasday:publish-counts
email@2.2.2
email@2.2.4
# testing
random@1.2.1
......
METEOR@2.8.1
METEOR@2.11.0
accounts-base@2.2.5
accounts-oauth@1.4.1
accounts-password@2.3.1
accounts-base@2.2.7
accounts-oauth@1.4.2
accounts-password@2.3.4
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.2
babel-compiler@7.10.3
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
......@@ -13,9 +13,9 @@ blaze-tools@1.1.3
boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
callback-hook@1.5.0
check@1.3.2
dburles:factory@1.1.0
dburles:factory@1.3.0
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
......@@ -23,16 +23,16 @@ ddp-rate-limiter@1.1.1
ddp-server@2.6.0
diff-sequence@1.1.2
dynamic-import@0.7.2
ecmascript@0.16.3
ecmascript@0.16.6
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.3
email@2.2.2
email@2.2.4
eoleteam:accounts-keycloak@2.1.0
eoleteam:keycloak-oauth@2.2.0
es5-shim@4.8.0
fetch@0.1.2
fetch@0.1.3
fourseven:scss@4.15.0
geojson-utils@1.0.11
hot-code-push@1.0.4
......@@ -45,36 +45,36 @@ inter-process-messaging@0.1.1
johanbrook:publication-collector@1.1.0
launch-screen@1.3.0
localstorage@1.2.0
logging@1.3.1
logging@1.3.2
mdg:validated-method@1.2.0
meteor@1.10.2
meteor@1.11.1
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:browser-tests@1.5.1
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mexar:mdt@0.2.2
minifier-js@2.7.5
minimongo@1.9.0
minimongo@1.9.2
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
modules-runtime-hot@0.14.1
mongo@1.16.1
mongo@1.16.5
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@4.11.0
oauth@2.1.2
oauth2@1.3.1
npm-mongo@4.14.0
oauth@2.2.0
oauth2@1.3.2
ordered-dict@1.1.0
promise@0.12.1
promise@0.12.2
raix:eventemitter@1.0.0
random@1.2.1
rate-limit@1.0.9
rdb:svelte-meteor-data@0.3.1
react-fast-refresh@0.2.3
react-fast-refresh@0.2.6
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
......@@ -90,11 +90,11 @@ svelte:compiler@3.46.4
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tmeasday:publish-counts@0.8.0
tracker@1.2.1
typescript@4.5.4
underscore@1.0.11
tracker@1.3.1
typescript@4.9.4
underscore@1.0.12
url@1.3.2
webapp@1.13.2
webapp@1.13.4
webapp-hashing@1.1.1
zodern:melte@1.6.1
zodern:melte-compiler@1.3.1
zodern:melte@1.7.0
zodern:melte-compiler@1.4.0
# The tag here should match the Meteor version of your app, per .meteor/release
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.8.1
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.11.0
# Copy app package.json and package-lock.json into container
#COPY ./app/package*.json $APP_SOURCE_FOLDER/
......@@ -14,7 +14,7 @@ RUN bash $SCRIPTS_FOLDER/build-meteor-bundle.sh
# Rather than Node 8 latest (Alpine), you can also use the specific version of Node expected by your Meteor release, per https://docs.meteor.com/changelog.html
FROM hub.eole.education/proxyhub/library/node:14.21.1-alpine
FROM hub.eole.education/proxyhub/library/node:14.21.3-alpine
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......@@ -37,7 +37,7 @@ RUN bash $SCRIPTS_FOLDER/build-meteor-npm-dependencies.sh --build-from-source
# Start another Docker stage, so that the final image doesn’t contain the layer with the build dependencies
# See previous FROM line; this must match
FROM hub.eole.education/proxyhub/library/node:14.21.1-alpine
FROM hub.eole.education/proxyhub/library/node:14.21.3-alpine
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......
# Changelog
## [1.7.0](https://gitlab.mim-libre.fr/alphabet/sondage/compare/release/1.6.0...release/1.7.0) (2023-04-18)
### Features
* **input validation:** validate user string inputs ([a3206c7](https://gitlab.mim-libre.fr/alphabet/sondage/commit/a3206c78fe4ed3956979342ebc6ce0bc55eb5865))
### Bug Fixes
* **answer:** add tooltip and center lock icon ([fb42ff8](https://gitlab.mim-libre.fr/alphabet/sondage/commit/fb42ff8cb18ced56f459fc84a1d4951ca336d80a))
* **events:** add createdAt/updatedAt to events schema ([2964a88](https://gitlab.mim-libre.fr/alphabet/sondage/commit/2964a88042371c8d39d2bda9271f851ca9479a72))
* **events:** change some meteor methods to internal backend functions ([365719a](https://gitlab.mim-libre.fr/alphabet/sondage/commit/365719aaafaccf26e35b09643672f0c64be3f9f2))
* **fullcalendar:** downgrade lib fullcalendar ([f3dc861](https://gitlab.mim-libre.fr/alphabet/sondage/commit/f3dc8612e9379af1b247a409845b9be4cea011e6))
* **fullcalendar:** downgrade lib fullcalendar ([db611d8](https://gitlab.mim-libre.fr/alphabet/sondage/commit/db611d8b4bdb04fa27cd408d76db80780f311544))
* **tooltip:** change tooltip declaration ([fb7fa44](https://gitlab.mim-libre.fr/alphabet/sondage/commit/fb7fa44d98a9050524b9d5ccec9491ae8bb5ed28))
## [1.6.0](https://gitlab.mim-libre.fr/alphabet/sondage/compare/release/1.5.0...release/1.6.0) (2023-01-30)
......
......@@ -106,6 +106,22 @@ EventsAgenda.schema = new SimpleSchema(
type: String,
optional: false,
},
createdAt: {
type: Date,
optional: true,
autoValue() {
if (this.isInsert) {
return new Date();
}
return this.value;
},
},
updatedAt: {
type: Date,
autoValue() {
return new Date();
},
},
},
{ tracker: Tracker },
);
......
......@@ -44,7 +44,7 @@ describe('events', function () {
describe('createEventAgendaMeeting', function () {
it('should create an event meeting into agenda with a connected user', function () {
createEventAgendaMeeting._execute({ userId: ownerPollUser._id }, { poll, answer: pollAnswer });
createEventAgendaMeeting(poll, pollAnswer, ownerPollUser._id);
const resultEvent = EventsAgenda.findOne({ title: poll.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
......@@ -55,7 +55,7 @@ describe('events', function () {
});
it('should create an event meeting into agenda with a guest user', function () {
const anotherPollAnswer = Factory.create('poll_answer', { email: 'toto@test.com' });
createEventAgendaMeeting._execute({ userId: ownerPollUser._id }, { poll, answer: anotherPollAnswer });
createEventAgendaMeeting(poll, anotherPollAnswer, ownerPollUser._id);
const resultEvent = EventsAgenda.findOne({ title: poll.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
......@@ -84,7 +84,7 @@ describe('events', function () {
});
Factory.create('poll_answer', { userId: null, pollId: pollGroup._id, email: 'toto@test.com' });
const date = new Date(Date.now() + 1000 * 60 * 60 * 24);
createEventAgenda._execute({ userId: ownerPollUser._id }, { poll: pollGroup, date });
createEventAgenda(pollGroup, date, ownerPollUser._id);
const resultEvent = EventsAgenda.findOne({ title: pollGroup.title });
assert.typeOf(resultEvent, 'object');
assert.typeOf(resultEvent.start, 'date');
......
import { Meteor } from 'meteor/meteor';
import SimpleSchema from 'simpl-schema';
import moment from 'moment';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import ical from 'ical-generator';
import PollsAnswers from '../../polls_answers/polls_answers';
import Polls from '../../polls/polls';
import Groups from '../../groups/groups';
import { DURATIONS_TIME, POLLS_TYPES, ROUTES } from '../../../utils/enums';
import {
......@@ -16,38 +13,31 @@ import {
} from './email_template';
import { EventsAgenda } from '../events';
export const sendEmail = new ValidatedMethod({
name: 'events.sendEmail',
validate: new SimpleSchema({
answer: PollsAnswers.schema,
poll: Polls.schema,
}).validator({ clean: true }),
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'),
});
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,
});
}
run({ 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'),
});
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,
});
},
});
export function sendEmailToCreator(poll, answer, userId) {
const template = adminMeetingTemplate;
const connected = userId !== null && userId !== undefined;
......@@ -101,74 +91,59 @@ export function sendEditEmail(poll, email, name, meetingSlot) {
});
}
export const createEventAgendaMeeting = new ValidatedMethod({
name: 'events.createMeeting',
validate: new SimpleSchema({
answer: PollsAnswers.schema,
poll: Polls.schema,
}).validator({ clean: true }),
run({ poll, answer }) {
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: this.userId,
});
},
});
export const createEventAgenda = new ValidatedMethod({
name: 'events.create',
validate: new SimpleSchema({
date: Date,
poll: Polls.schema,
}).validator({ clean: true }),
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,
});
}
run({ poll, date }) {
let answers = [];
if (poll.public) {
answers = PollsAnswers.find({ userId: null, pollId: poll._id }).fetch();
}
const groups = Groups.find({ _id: { $in: poll.groups } }).fetch();
const participants = groups
.map(({ _id, admins, animators, members }) =>
Meteor.users
.find({ _id: { $in: [...admins, ...animators, ...members] } })
.fetch()
.map((user) => ({
_id: user._id,
email: user.emails[0].address,
groupId: _id,
status: 1,
})),
)
.flat(1)
.filter((v, i, a) => a.findIndex((t) => t._id === v._id) === i); // remove duplicate participants based on _id prop
EventsAgenda.insert({
title: poll.title,
location: '',
start: moment(date).format(),
end: moment(date).add(DURATIONS_TIME[poll.duration], 'minute').format(),
allDay: poll.allDay,
participants,
guests: answers.map(({ email }) => email),
description: poll.description,
groups: groups.map(({ _id, name }) => ({ _id, name })),
userId: this.userId,
});
},
});
export function createEventAgenda(poll, date, userId) {
let answers = [];
if (poll.public) {
answers = PollsAnswers.find({ userId: null, pollId: poll._id }).fetch();
}
const groups = Groups.find({ _id: { $in: poll.groups } }).fetch();
const participants = groups
.map(({ _id, admins, animators, members }) =>
Meteor.users
.find({ _id: { $in: [...admins, ...animators, ...members] } })
.fetch()
.map((user) => ({
_id: user._id,
email: user.emails[0].address,
groupId: _id,
status: 1,
})),
)
.flat(1)
.filter((v, i, a) => a.findIndex((t) => t._id === v._id) === i); // remove duplicate participants based on _id prop
EventsAgenda.insert({
title: poll.title,
location: '',
start: moment(date).format(),
end: moment(date).add(DURATIONS_TIME[poll.duration], 'minute').format(),
allDay: poll.allDay,
participants,
guests: answers.map(({ email }) => email),
description: poll.description,
groups: groups.map(({ _id, name }) => ({ _id, name })),
userId,
});
}
......@@ -4,6 +4,18 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import Polls from './polls';
import PollsAnswers from '../polls_answers/polls_answers';
import validateString from '../../utils/functions/strings';
export const validatePoll = (data) => {
validateString(data.title);
if (data.description) validateString(data.description);
if (data.duration) validateString(data.duration);
if (data.dates) {
data.dates.forEach((date) => {
if (date.slots) date.slots.forEach((slot) => validateString(slot));
});
}
};
export const createPoll = new ValidatedMethod({
name: 'polls.create',
......@@ -16,13 +28,17 @@ export const createPoll = new ValidatedMethod({
if (!this.userId) {
throw new Meteor.Error('api.polls.methods.create.notLoggedIn', 'api.errors.notLoggedIn');
}
validatePoll(data);
return Polls.insert({ ...data, userId: this.userId });
},
});
export const removePolls = new ValidatedMethod({
name: 'polls.remove',
validate: new SimpleSchema({
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator({ clean: true }),
run({ pollId }) {
......@@ -44,7 +60,10 @@ export const removePolls = new ValidatedMethod({
export const toggleActivePoll = new ValidatedMethod({
name: 'polls.toggleActivePoll',
validate: new SimpleSchema({
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator({ clean: true }),
run({ pollId }) {
......
......@@ -8,11 +8,15 @@ import Polls from '../polls';
import Groups from '../../groups/groups';
import PollsAnswers from '../../polls_answers/polls_answers';
import { sendEmail, createEventAgenda } from '../../events/server/methods';
import { validatePoll } from '../methods';
export const getSinglePoll = new ValidatedMethod({
name: 'polls.getSinglePoll',
validate: new SimpleSchema({
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator({ clean: true }),
run({ pollId }) {
......@@ -23,7 +27,10 @@ export const getSinglePoll = new ValidatedMethod({
export const getSinglePollToAnswer = new ValidatedMethod({
name: 'polls.getSinglePollToAnswer',
validate: new SimpleSchema({
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator({ clean: true }),
run({ pollId }) {
......@@ -58,7 +65,10 @@ export const updatePoll = new ValidatedMethod({
name: 'polls.update',
validate: new SimpleSchema({
data: Polls.schema.omit('createdAt', 'updatedAt'),
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator({ clean: true }),
run({ data, pollId }) {
......@@ -73,7 +83,7 @@ export const updatePoll = new ValidatedMethod({
} else if (poll.active || poll.completed) {
throw new Meteor.Error('api.polls.methods.update.active', 'api.errors.notAllowed');
}
validatePoll(data);
return Polls.update({ _id: pollId }, { $set: { ...data } });
},
});
......@@ -81,7 +91,10 @@ export const updatePoll = new ValidatedMethod({
export const validatePollAnswer = new ValidatedMethod({
name: 'polls.validate',
validate: new SimpleSchema({
pollId: String,
pollId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
date: Date,
}).validator({ clean: true }),
......@@ -96,18 +109,15 @@ export const validatePollAnswer = new ValidatedMethod({
if (poll.public) {
const answers = PollsAnswers.find({ userId: null, pollId }).fetch();
answers.forEach((answer) =>
sendEmail.call({
poll,
answer: {
...answer,
meetingSlot: date,
},
sendEmail(poll, {
...answer,
meetingSlot: date,
}),
);
}
if (poll.groups.length) {
createEventAgenda._execute({ userId: this.userId }, { poll, date });
createEventAgenda(poll, date, this.userId);
if (!Meteor.isTest) {
// eslint-disable-next-line global-require
const sendnotif = require('../../notifications/server/notifSender').default;
......
......@@ -14,6 +14,7 @@ import {
sendEditEmail,
sendEmailToCreator,
} from '../../events/server/methods';
import validateString from '../../../utils/functions/strings';
export const createPollAnswers = new ValidatedMethod({
name: 'polls_answers.create',
......@@ -39,6 +40,13 @@ export const createPollAnswers = new ValidatedMethod({
} else if (poll.completed) {
throw new Meteor.Error('api.polls_answers.methods.create.notAllowed', 'api.errors.notAllowed');
}
validateString(data.email);
validateString(data.name);
if (data.choices) {
data.choices.forEach((choice) => {
if (choice.slots) choice.slots.forEach((slot) => validateString(slot));
});
}
if (!poll.public && this.userId) {
const isInAGroup = Groups.findOne(
{
......@@ -99,8 +107,8 @@ export const validateMeetingPollAnswer = new ValidatedMethod({
throw new Meteor.Error('api.polls_answers.methods.validate.notAllowed', 'api.errors.notAllowed');
}
sendEmail.call({ poll, answer });
createEventAgendaMeeting._execute({ userId: this.userId }, { poll, answer });
sendEmail(poll, answer);
createEventAgendaMeeting(poll, answer, this.userId);
return PollsAnswers.update({ _id: answerId }, { $set: { confirmed: true } });
},
......@@ -150,6 +158,8 @@ export const editMeetingPollAnswer = new ValidatedMethod({
if (poll.userId !== this.userId || poll.type !== POLLS_TYPES.MEETING) {
throw new Meteor.Error('api.polls_answers.methods.edit.notAllowed', 'api.errors.notAllowed');
}
validateString(email);
validateString(name);
if (emailNotice) sendEditEmail(poll, email, name, meetingSlot);
return PollsAnswers.update({ _id: answerId }, { $set: { email, name, meetingSlot } });
},
......
<script>
import { _ } from "svelte-i18n";
import tippy from "sveltejs-tippy";
import { toggleActivePoll, removePolls } from "../../../api/polls/methods";
import { useTracker } from "meteor/rdb:svelte-meteor-data";
import { ROUTES, POLLS_TYPES, toasts } from "../../../utils/enums";
import copy from "copy-to-clipboard";
import { toast } from "@zerodevx/svelte-toast";
import Modal from "../../components/common/Modal.svelte";
import { _ } from 'svelte-i18n';
import { toggleActivePoll, removePolls } from '../../../api/polls/methods';
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import { ROUTES, POLLS_TYPES, toasts } from '../../../utils/enums';
import copy from 'copy-to-clipboard';
import { toast } from '@zerodevx/svelte-toast';
import Modal from '../../components/common/Modal.svelte';
export let poll;
let votes;
......@@ -14,14 +13,14 @@
const tooltip = (content) => ({
content,
placement: "bottom",
placement: 'bottom',
});
const togglePoll = () => {
toggleActivePoll.call({ pollId: poll._id });
};
const toggleModal = () => (removePollModal = !removePollModal);
$: votes = useTracker(() => {
Meteor.subscribe("polls_answers.getCount", { pollId: poll._id });
Meteor.subscribe('polls_answers.getCount', { pollId: poll._id });
return Counts.get(`polls_answers.get-${poll._id}`);
});
const removePoll = () => {
......@@ -33,11 +32,9 @@
});
};
const copyToClipboard = () => {
const url = `${Meteor.absoluteUrl()}${ROUTES.ANSWER_POLL_RM(
poll._id
).replace("/", "")}`;
const url = `${Meteor.absoluteUrl()}${ROUTES.ANSWER_POLL_RM(poll._id).replace('/', '')}`;
copy(url);
toast.push($_("components.SinglePollLine.copied"));
toast.push($_('components.SinglePollLine.copied'));
};
</script>
......@@ -49,7 +46,7 @@
class="button is-small is-primary"
value={poll._id}
on:click={togglePoll}
use:tippy={tooltip($_("pages.home.deactivate_tooltip"))}
title={$_('pages.home.deactivate_tooltip')}
>
<i class="fas fa-eye" />
</button>
......@@ -63,37 +60,29 @@
disabled={poll.completed}
value={poll._id}
on:click={togglePoll}
use:tippy={tooltip($_("pages.home.activate_tooltip"))}
title={$_('pages.home.activate_tooltip')}
>
<i class="fas fa-eye-slash" />
</button>
{/if}
<a
href={!poll.active && $votes === 0
? ROUTES.EDIT_POLL_RM(poll._id)
: ROUTES.ADMIN}
>
<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}
use:tippy={tooltip($_("pages.home.edit_tooltip"))}
title={$_('pages.home.edit_tooltip')}
>
<i class="fas fa-pen" />
</button>
</a>
<button
class="button is-small is-info"
use:tippy={tooltip($_("pages.home.link_tooltip"))}
on:click={copyToClipboard}
>
<button class="button is-small is-info" title={$_('pages.home.link_tooltip')} on:click={copyToClipboard}>
<i class="far fa-copy" />
</button>
<a
href={ROUTES.ANSWER_POLL_RM(poll._id)}
class="button is-small is-primary"
use:tippy={tooltip($_("pages.home.open_tooltip"))}
title={$_('pages.home.open_tooltip')}
>
<i class="fas fa-external-link-alt" />
</a>
......@@ -101,7 +90,7 @@
class="button is-small is-danger"
on:click={toggleModal}
disabled={poll.active}
use:tippy={tooltip($_("pages.home.delete_tooltip"))}
title={$_('pages.home.delete_tooltip')}
>
<i class="fas fa-trash" />
</button>
......@@ -136,12 +125,12 @@
toggle={toggleModal}
active={removePollModal}
action={removePoll}
title={$_("pages.home.remove_title")}
validButton={$_("pages.home.remove_valid")}
title={$_('pages.home.remove_title')}
validButton={$_('pages.home.remove_valid')}
validClass="is-danger"
cancelButton={$_("pages.home.remove_cancel")}
cancelButton={$_('pages.home.remove_cancel')}
cancelClass="is-white"
>
<p>{$_("pages.home.remove_text")}</p>
<p>{$_('pages.home.remove_text')}</p>
<p><b>{poll.title}</b></p>
</Modal>
......@@ -16,7 +16,7 @@
import CalendarPoll from './CalendarPoll.svelte';
import PollDateTable from './PollDateTable.svelte';
import { POLLS_TYPES } from '../../../utils/enums';
import getGroupName from '/imports/utils/functions/groups'
import getGroupName from '/imports/utils/functions/groups';
import MeetingAnswersList from './MeetingAnswersList.svelte';
import PackageJSON from '../../../../package.json';
import Modal from '../../components/common/Modal.svelte';
......@@ -140,7 +140,7 @@
{#if poll.title}
{poll.title}
{#if !poll.active}
<span class="icon is-small">
<span title={$_('api.errors.pollNotActive')}>
<i class="fas fa-lock" />
</span>
{/if}
......
<script>
import { Meteor } from "meteor/meteor";
import DateDisplay from "../../components/common/DateDisplay.svelte";
import Checkbox from "../../components/common/Checkbox.svelte";
import Modal from "../../components/common/Modal.svelte";
import { useTracker } from "meteor/rdb:svelte-meteor-data";
import { toast } from "@zerodevx/svelte-toast";
import { _ } from "svelte-i18n";
import tippy from "sveltejs-tippy";
import PollsAnswers from "../../../api/polls_answers/polls_answers";
import moment from "moment";
import { toasts } from "../../../utils/enums";
import { Meteor } from 'meteor/meteor';
import DateDisplay from '../../components/common/DateDisplay.svelte';
import Checkbox from '../../components/common/Checkbox.svelte';
import Modal from '../../components/common/Modal.svelte';
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import { toast } from '@zerodevx/svelte-toast';
import { _ } from 'svelte-i18n';
import PollsAnswers from '../../../api/polls_answers/polls_answers';
import moment from 'moment';
import { toasts } from '../../../utils/enums';
export let answer = {};
export let poll = {};
......@@ -20,16 +19,16 @@
let chosenDateModal;
let answers;
$: answers = useTracker(() => {
Meteor.subscribe("polls_answers.onePoll", { pollId: poll._id });
Meteor.subscribe('polls_answers.onePoll', { pollId: poll._id });
return PollsAnswers.find({ pollId: poll._id }).fetch();
});
const toggleModal = (date) => {
confirmDateModal = !confirmDateModal;
chosenDateModal = date;
}
};
const confirmDate = (date) => {
loading = true;
Meteor.call("polls.validate", { pollId: poll._id, date }, (e, r) => {
Meteor.call('polls.validate', { pollId: poll._id, date }, (e, r) => {
loading = false;
grabData();
if (e) {
......@@ -65,51 +64,13 @@
<tr>
<th />
<!-- {#each poll.dates as day} -->
{#if !poll.allDay}
{#each poll.dates as day}
{#each day.slots as time}
<th>
{#if !poll.completed || moment(day.date)
.hour(time.split(":")[0])
.minute(time.split(":")[1])
.isSame(poll.choosenDate)}
<button
disabled={poll.completed}
class="button is-fullwidth"
class:is-loading={loading}
class:is-primary={!poll.completed}
class:is-success={moment(day.date)
.hour(time.split(":")[0])
.minute(time.split(":")[1])
.isSame(poll.choosenDate)}
use:tippy={{
content: $_("pages.answer.confirm_date_tooltip"),
placement: "bottom",
}}
on:click={() =>
poll.completed
? null
: toggleModal(
moment(day.date)
.hour(time.split(":")[0])
.minute(time.split(":")[1])
.format()
)}
>
{#if !poll.completed}
{$_("pages.answer.confirm_date")}
{:else}
{$_("pages.answer.choosen_date")}
{/if}
</button>
{/if}
</th>
{/each}
{/each}
{:else}
{#each poll.dates as day}
{#if !poll.allDay}
{#each poll.dates as day}
{#each day.slots as time}
<th>
{#if !poll.completed || moment(day.date)
.hour(time.split(':')[0])
.minute(time.split(':')[1])
.isSame(poll.choosenDate)}
<button
disabled={poll.completed}
......@@ -117,29 +78,48 @@
class:is-loading={loading}
class:is-primary={!poll.completed}
class:is-success={moment(day.date)
.hour(time.split(':')[0])
.minute(time.split(':')[1])
.isSame(poll.choosenDate)}
use:tippy={{
content: $_("pages.answer.confirm_date_tooltip"),
placement: "bottom",
}}
title={$_('pages.answer.confirm_date_tooltip')}
on:click={() =>
poll.completed
? null
: toggleModal(
moment(day.date)
.format()
)}
: toggleModal(moment(day.date).hour(time.split(':')[0]).minute(time.split(':')[1]).format())}
>
{#if !poll.completed}
{$_("pages.answer.confirm_date")}
{$_('pages.answer.confirm_date')}
{:else}
{$_("pages.answer.choosen_date")}
{$_('pages.answer.choosen_date')}
{/if}
</button>
{/if}
</th>
{/each}
{/if}
{/each}
{:else}
{#each poll.dates as day}
<th>
{#if !poll.completed || moment(day.date).isSame(poll.choosenDate)}
<button
disabled={poll.completed}
class="button is-fullwidth"
class:is-loading={loading}
class:is-primary={!poll.completed}
class:is-success={moment(day.date).isSame(poll.choosenDate)}
title={$_('pages.answer.confirm_date_tooltip')}
on:click={() => (poll.completed ? null : toggleModal(moment(day.date).format()))}
>
{#if !poll.completed}
{$_('pages.answer.confirm_date')}
{:else}
{$_('pages.answer.choosen_date')}
{/if}
</button>
{/if}
</th>
{/each}
{/if}
<!-- {/each} -->
</tr>
</tfoot>
......@@ -150,7 +130,7 @@
<td>
<b>
{answer._id ? [...$answers, answer].length : $answers.length}
{$_("pages.answer.participants")}
{$_('pages.answer.participants')}
</b>
</td>
{#each poll.dates as day, index}
......@@ -174,7 +154,7 @@
<tr class="is-selected">
<td>
{answer.name ? answer.name : $_("pages.answer.name")}
{answer.name ? answer.name : $_('pages.answer.name')}
</td>
{#each poll.dates as day, index}
{#if !poll.allDay && day.slots}
......@@ -207,13 +187,7 @@
{#if !poll.allDay && day.slots}
{#each day.slots as slo, i}
<td>
<Checkbox
center
disabled
checked={single_answer.choices[index].slots.find(
(s) => s === slo
)}
/>
<Checkbox center disabled checked={single_answer.choices[index].slots.find((s) => s === slo)} />
</td>
{/each}
{:else}
......@@ -221,8 +195,7 @@
<Checkbox
center
disabled
checked={single_answer.choices[index] &&
single_answer.choices[index].present}
checked={single_answer.choices[index] && single_answer.choices[index].present}
/>
</td>
{/if}
......@@ -237,16 +210,16 @@
toggle={toggleModal}
active={confirmDateModal}
action={() => confirmDate(chosenDateModal)}
title={$_("pages.answer.modal_title")}
validButton={$_("pages.answer.modal_valid")}
title={$_('pages.answer.modal_title')}
validButton={$_('pages.answer.modal_valid')}
validClass="is-success"
cancelButton={$_("pages.answer.modal_cancel")}
cancelButton={$_('pages.answer.modal_cancel')}
cancelClass="is-white"
>
<p>{$_("pages.answer.modal_text")}</p>
{#if poll.allDay}
<p>{$_('pages.answer.modal_text')}</p>
{#if poll.allDay}
<DateDisplay date={chosenDateModal} duration={poll.duration} />
{:else}
{:else}
<DateDisplay date={chosenDateModal} slot={moment(chosenDateModal).format('LT')} duration={poll.duration} />
{/if}
{/if}
</Modal>
<script>
import { _ } from 'svelte-i18n';
import moment from 'moment';
import tippy from 'sveltejs-tippy';
import { toast } from '@zerodevx/svelte-toast';
import { toasts, ROUTES } from '../../../utils/enums';
......@@ -33,11 +32,9 @@
class:is-success={answer.confirmed}
class:is-primary={!answer.confirmed}
on:click={validateMeeting}
use:tippy={tooltip(
answer.confirmed
? $_('components.SinglePollAnswerLine.validated_tooltip')
: $_('components.SinglePollAnswerLine.validate_tooltip'),
)}
title={answer.confirmed
? $_('components.SinglePollAnswerLine.validated_tooltip')
: $_('components.SinglePollAnswerLine.validate_tooltip')}
>
<span class="icon">
<i class="fas" class:fa-calendar-check={answer.confirmed} class:fa-paper-plane={!answer.confirmed} />
......
......@@ -73,8 +73,7 @@
messages = error.details.map((err) => err.message);
toast.push(messages.join('<br/>'), toasts.error);
} else {
console.log(error);
toast.push($_(error.message || error.reason || 'pages.new_poll_4.creationError'), toasts.error);
toast.push($_(error.reason || error.message || 'pages.new_poll_4.creationError'), toasts.error);
}
} else {
toast.push($_(meta.params._id ? 'pages.new_poll_4.poll_modified' : 'pages.new_poll_4.poll_created'));
......
<script>
import { _ } from "svelte-i18n";
import moment from "moment";
import tippy from "sveltejs-tippy";
import { useTracker } from "meteor/rdb:svelte-meteor-data";
import { ROUTES } from "/imports/utils/enums";
import copy from "copy-to-clipboard";
import { toast } from "@zerodevx/svelte-toast";
import { POLLS_TYPES } from "../../../utils/enums";
import PollsAnswers from "../../../api/polls_answers/polls_answers";
import { _ } from 'svelte-i18n';
import moment from 'moment';
import { useTracker } from 'meteor/rdb:svelte-meteor-data';
import { ROUTES } from '/imports/utils/enums';
import copy from 'copy-to-clipboard';
import { toast } from '@zerodevx/svelte-toast';
import { POLLS_TYPES } from '../../../utils/enums';
import PollsAnswers from '../../../api/polls_answers/polls_answers';
export let poll;
let typeDataVote;
const tooltip = (content) => ({
content,
placement: "bottom",
placement: 'bottom',
});
$: typeDataVote = useTracker(() => {
if (poll.type === POLLS_TYPES.POLL) {
Meteor.subscribe("polls_answers.getCount", { pollId: poll._id });
Meteor.subscribe('polls_answers.getCount', { pollId: poll._id });
return Counts.get(`polls_answers.get-${poll._id}`);
} else if (poll.type === POLLS_TYPES.MEETING) {
Meteor.subscribe("polls_answers.getCurrentUser", { pollId: poll._id });
Meteor.subscribe('polls_answers.getCurrentUser', { pollId: poll._id });
return PollsAnswers.findOne({ pollId: poll._id });
}
});
const copyToClipboard = () => {
const url = `${Meteor.absoluteUrl()}${ROUTES.ANSWER_POLL_RM(
poll._id
).replace("/", "")}`;
const url = `${Meteor.absoluteUrl()}${ROUTES.ANSWER_POLL_RM(poll._id).replace('/', '')}`;
copy(url);
toast.push($_("components.SinglePollLine.copied"));
toast.push($_('components.SinglePollLine.copied'));
};
</script>
<tr>
<th>
<div class="buttons">
<button
class="button is-small is-info"
use:tippy={tooltip($_("pages.home.link_tooltip"))}
on:click={copyToClipboard}
>
<button class="button is-small is-info" title={$_('pages.home.link_tooltip')} on:click={copyToClipboard}>
<i class="far fa-copy" />
</button>
<a
href={ROUTES.ANSWER_POLL_RM(poll._id)}
class="button is-small is-primary"
use:tippy={tooltip($_("pages.home.open_tooltip"))}
title={$_('pages.home.open_tooltip')}
>
<i class="fas fa-external-link-alt" />
</a>
......@@ -69,9 +62,9 @@
<td> {$typeDataVote} </td>
{:else if poll.type === POLLS_TYPES.MEETING}
{#if $typeDataVote}
<td> {moment($typeDataVote.meetingSlot).format("LLL")} </td>
<td> {moment($typeDataVote.meetingSlot).format('LLL')} </td>
{:else}
<td> {$_("polls_datas.noAnswer")} </td>
<td> {$_('polls_datas.noAnswer')} </td>
{/if}
{/if}
</tr>
import { Meteor } from 'meteor/meteor';
const regValidateStrict = /[<>"'&]/g;
const regValidate = /((<|%3C|&lt;)script)|(('|"|%22|%27) *on[a-z_]+ *(=|%3D))/gi;
/** Check a string for malicious content */
const validateString = (content, strict = false) => {
if (content.length > 50000) {
throw new Meteor.Error('api.utils.functions.strings.validateString.tooLong', 'api.utils.stringTooLong');
}
/** strict forbids any of the following characters : < > " ' &
otherwise, forbid script tags and pattern like " onload=... */
const scriptRegex = strict ? regValidateStrict : regValidate;
if (content.match(scriptRegex) !== null) {
throw new Meteor.Error(
'api.utils.functions.strings.validateString.error',
strict ? 'api.utils.badCharsDetected' : 'api.utils.scriptDetected',
);
}
return content;
};
export default validateString;
......@@ -25,6 +25,11 @@
},
"tags": {
"titleValidation": "Choose a slot then validate your choice"
},
"utils": {
"scriptDetected": "Forbidden content has been detected",
"badCharsDetected": "Following characters are Forbidden: < > & ' \"",
"stringTooLong": "String is too long (max 50k)"
}
},
"links": {
......