Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alphabet/laboite
  • arthur.lutz/laboite
  • daniel.dehennin/laboite
  • olivier.heintz/laboite
4 results
Show changes
Commits on Source (2142)
Showing
with 766 additions and 115 deletions
.env
config/**.development.json
\ No newline at end of file
config/**.development.json
config/settings.development.json*
!config/settings.development.json.sample
.DS_Store
app/.reports
**/.reports/*.*
passwords.txt
provisionning/.env
provisionning/*.csv
provisionning/csv
__pycache__/
services/.env
......@@ -33,7 +33,7 @@ stages:
# Common setup for all meteor based jobs
.meteor:
extends: .not-on-stable
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.7.1
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.16
before_script:
- cd app
cache:
......@@ -49,7 +49,6 @@ stages:
variables:
METEOR_ALLOW_SUPERUSER: "true"
###############################################################################
# `initial-checks` stage: `commitlint`
###############################################################################
......@@ -58,7 +57,6 @@ commitlint:
stage: initial-checks
extends: .git:commitlint
###############################################################################
# `deps` stage: to download and cache dependencies
###############################################################################
......@@ -69,8 +67,7 @@ cache-dependencies:
# This job update dependencies
policy: pull-push
script:
- meteor npm ci && meteor npm install
- meteor npm ci
###############################################################################
# `test` stage: `meteor-lint`, `meteor-tests`
......@@ -90,7 +87,6 @@ meteor-tests:
script:
- meteor npm test
###############################################################################
# `build` stage: `*-docker-build`
###############################################################################
......@@ -115,7 +111,6 @@ build-docker:
- !reference [.rules-map, not-on-semantic-release-commit]
- !reference [.rules-map, on-branch]
###############################################################################
# `release` stage: `semantic-release`, `testing-prerelease`, `merge-to-dev`, `tag *`
###############################################################################
......@@ -126,7 +121,8 @@ testing prerelease:
extends: .semantic-release:testing
# Avoid regression and update `version` of app/package*.json in `$DEV_BRANCH`
merge-to-dev: {extends: '.git:merge-to', variables: {GIT_MERGE_TARGET: $DEV_BRANCH}}
merge-to-dev:
{ extends: ".git:merge-to", variables: { GIT_MERGE_TARGET: $DEV_BRANCH } }
## tag contribution branches with a more stable name than `git-${CI_COMMIT_SHORT_SHA}`
tag contrib branch:
......
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# ...
[ -z "$CI" ] || exit 0
cd app
npm run lint ./
npx jscpd
\ No newline at end of file
npx jscpd --gitignore
npm run check-lang
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# ...
[ -z "$CI" ] || exit 0
cd app
npm run test
\ No newline at end of file
# The tag here should match the Meteor version of your app, per .meteor/release
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.7.1
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.16
# Copy app package.json and package-lock.json into container
COPY ./app/package*.json $APP_SOURCE_FOLDER/
......@@ -10,12 +10,9 @@ RUN bash $SCRIPTS_FOLDER/build-app-npm-dependencies.sh
COPY ./app $APP_SOURCE_FOLDER/
RUN bash $SCRIPTS_FOLDER/build-meteor-bundle.sh
# run npm install after initial installation because resolutions
# are not taken in account with npm ci
RUN cd $APP_SOURCE_FOLDER && meteor npm install
# 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.19.1-alpine
FROM hub.eole.education/proxyhub/meteor/node:14.21.4-alpine3.17
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......@@ -38,7 +35,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.19.1-alpine
FROM hub.eole.education/proxyhub/meteor/node:14.21.4-alpine3.17
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......
FROM python:3-alpine
ENV DATA_FOLDER /lifeCycleService
RUN apk --no-cache add py3-pip git
# Copy app scripts
COPY ./services/requirements.txt $DATA_FOLDER/
COPY ./services/lifecycle.py $DATA_FOLDER/
RUN pip3 install -r $DATA_FOLDER/requirements.txt
......@@ -9,6 +9,39 @@ Développé dans le cadre du projet [APPS-EDUCATION](https://apps.education.fr/)
Plus d'information sur le [ wiki](https://gitlab.mim-libre.fr/alphabet/laboite/-/wikis/home).
Utilise :
- [METEOR](https://www.meteor.com)
- [REACT](https://fr.reactjs.org/)
- [METEOR](https://www.meteor.com)
- [REACT](https://fr.reactjs.org/)
## Initialisation
- Le projet nécessite l'installation de Meteor.
https://www.meteor.com/developers/install
## Installation en local
- Se placer dans le dossier app du projet.
- Facultatif: Si présent, supprimer le dossier node_modules.
- Utiliser la commande suivante, toujours dans le dossier app:
`meteor npm ci`
- Une fois l'installation des packages effectuées, se rendre dans le dossier config du projet.
- Copier le fichier settings.development.json.sample et le coller au même endroit, en le renommant settings.development.json.
- Remplir le fichier settings.development.json avec les informations utiles.
## fichier settings.development.json
ATTENTION: Pour les développeurs, ne jamais publier le fichier settings.development.json et ne jamais remplir et publier le fichier settings.development.json.sample.
La description des champs et les valeurs requisent sont toutes définies dans le fichier README.md présent dans le dossier config.
## Lancement du projet
- Lorsque tout est convenablement installé et configuré, revenir dans le dossier app, puis exécuter la commande suivant:
`meteor npm start`
## Contribution au projet
[Guide développeur](https://gitlab.mim-libre.fr/alphabet/laboite/-/blob/dev/doc/Developer%20guide/Contribuer%20au%20projet.md)
......@@ -19,22 +19,25 @@
"settings": {
"import/resolver": {
"meteor": {
"extensions": [".js", ".jsx"]
"extensions": [".js", ".jsx", ".meteor.js"]
}
}
},
"globals": {
"msg": false
"msg": false,
"globalThis": false
},
"plugins": ["meteor", "import", "prettier", "i18n", "react", "jsx"],
"ignorePatterns": ["packages/**/*.js", "packages/**/*.jsx"],
"ignorePatterns": ["packages/**/*.js", "packages/**/*.jsx", "private/widget/"],
"rules": {
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
"no-restricted-imports": ["error", "@mui/material", "@mui/icons-material", "@material-ui/lab"],
"no-underscore-dangle": "off",
"react/no-danger": "off",
"react/jsx-props-no-spreading": "off",
"react/forbid-prop-types": "off",
"react/require-default-props": "off",
"no-nested-ternary": "off",
"prettier/prettier": "error",
"max-len": [
......
node_modules/
**/.reports/*.*
{
"threshold": 0.2,
"threshold": 0.25,
"reporters": ["console"],
"ignore": [
"**/node_modules/**",
"**/.meteor/**",
"./packages/**",
"./i18n/*.json",
"**/.meteor/**",
"**/.reports/**",
"**/private/**",
"./packages/**",
"./i18n/*.json",
"**/*.svg",
"**.test.js",
"*/**/**/themes",
"*/**/**.tests.jsx",
"*/**/**.tests.js"
],
......
......@@ -5,14 +5,14 @@
# but you can also edit it by hand.
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.14.6 # The database Meteor supports right now
reactive-var@1.0.11 # Reactive variable for tracker
mobile-experience@1.1.1 # Packages for a great mobile UX
mongo@1.16.10 # The database Meteor supports right now
reactive-var@1.0.12 # Reactive variable for tracker
standard-minifier-css@1.8.1 # CSS minifier run for production mode
standard-minifier-js@2.8.0 # JS minifier run for production mode
standard-minifier-css@1.9.2 # CSS minifier run for production mode
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.2 # Enable ECMAScript2015+ syntax in app code
ecmascript@0.16.8 # Enable ECMAScript2015+ syntax in app code
shell-server@0.5.0 # Server-side component of the `meteor shell` command
static-html@1.3.2 # Define static page content in .html files
......@@ -22,9 +22,9 @@ aldeed:collection2
aldeed:schema-index
mdg:validated-method
universe:i18n
hot-module-replacement@0.5.1
hot-module-replacement@0.5.3
accounts-password@2.3.1
accounts-password@2.4.0
alanning:roles
# testing
......@@ -32,17 +32,20 @@ meteortesting:mocha
hwillson:stub-collections
johanbrook:publication-collector
dburles:factory
service-configuration@1.3.0
reywood:publish-composite
service-configuration@1.3.4
percolate:find-from-publication
percolate:migrations
matb33:collection-hooks
seba:method-hooks
email@2.2.1
check@1.3.1
email@2.2.6
check@1.4.1
tmeasday:publish-counts
eoleteam:accounts-keycloak
mexar:mdt
random
server-render
random@1.2.1
server-render@0.4.1
logging@1.3.4
mizzao:user-status
littledata:synced-cron
sakulstra:aggregate
eoleteam:method-hooks
METEOR@2.7.1
METEOR@2.16
accounts-base@2.2.2
accounts-oauth@1.4.1
accounts-password@2.3.1
alanning:roles@3.4.0
accounts-base@2.2.11
accounts-oauth@1.4.4
accounts-password@2.4.0
alanning:roles@3.6.3
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
aldeed:schema-index@3.1.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.0
babel-runtime@1.5.0
babel-compiler@7.10.5
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.2
boilerplate-generator@1.7.1
blaze-tools@1.1.4
boilerplate-generator@1.7.2
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
caching-html-compiler@1.2.2
callback-hook@1.5.1
check@1.4.1
dburles:collection-helpers@1.1.0
dburles:factory@1.1.0
ddp@1.4.0
ddp-client@2.5.0
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.2
ecmascript-runtime@0.8.0
dburles:factory@1.5.0
ddp@1.4.1
ddp-client@2.6.2
ddp-common@1.4.1
ddp-rate-limiter@1.2.1
ddp-server@2.7.1
diff-sequence@1.1.2
dynamic-import@0.7.3
ecmascript@0.16.8
ecmascript-runtime@0.8.1
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.2
email@2.2.1
ejson@1.1.3
email@2.2.6
eoleteam:accounts-keycloak@2.1.0
eoleteam:keycloak-oauth@2.2.0
eoleteam:keycloak-oauth@2.3.0
eoleteam:method-hooks@3.0.5
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
fetch@0.1.4
geojson-utils@1.0.11
hot-code-push@1.0.4
hot-module-replacement@0.5.1
html-tools@1.1.2
htmljs@1.1.1
hot-module-replacement@0.5.3
html-tools@1.1.4
htmljs@1.2.1
http@2.0.0
hwillson:stub-collections@1.0.9
id-map@1.1.1
inter-process-messaging@0.1.1
johanbrook:publication-collector@1.1.0
launch-screen@1.3.0
launch-screen@2.0.0
littledata:synced-cron@1.5.1
localstorage@1.2.0
logging@1.3.1
matb33:collection-hooks@1.1.2
mdg:validated-method@1.2.0
meteor@1.10.0
logging@1.3.4
matb33:collection-hooks@1.3.1
mdg:validated-method@1.3.0
meteor@1.11.5
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:browser-tests@1.5.3
meteortesting:mocha@2.1.0
meteortesting:mocha-core@8.1.2
mexar:mdt@0.2.2
minifier-css@1.6.0
minifier-js@2.7.4
minimongo@1.8.0
mobile-experience@1.1.0
minifier-css@1.6.4
minifier-js@2.8.0
minimongo@1.9.4
mizzao:timesync@0.5.5
mizzao:user-status@1.1.0
mobile-experience@1.1.1
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.13.0
modules-runtime-hot@0.14.0
mongo@1.14.6
mongo-decimal@0.1.2
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
modules-runtime-hot@0.14.2
mongo@1.16.10
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@4.3.1
oauth@2.1.2
oauth2@1.3.1
npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
ordered-dict@1.1.0
percolate:find-from-publication@0.2.1
percolate:migrations@1.0.3
promise@0.12.0
percolate:migrations@1.1.1
promise@0.12.2
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.2.3
react-meteor-data@2.4.0
reactive-var@1.0.11
random@1.2.1
rate-limit@1.1.1
react-fast-refresh@0.2.8
react-meteor-data@2.7.2
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
reywood:publish-composite@1.7.3
routepolicy@1.1.1
seba:method-hooks@3.0.2
server-render@0.4.0
service-configuration@1.3.0
sakulstra:aggregate@1.4.4
server-render@0.4.1
service-configuration@1.3.4
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
standard-minifier-css@1.8.1
standard-minifier-js@2.8.0
socket-stream-client@0.5.2
spacebars-compiler@1.3.2
standard-minifier-css@1.9.2
standard-minifier-js@2.8.1
static-html@1.3.2
templating-tools@1.2.1
templating-tools@1.2.3
tmeasday:check-npm-versions@1.0.2
tmeasday:publish-counts@0.8.0
tracker@1.2.0
typescript@4.5.4
underscore@1.0.10
tracker@1.3.3
typescript@4.9.5
underscore@1.6.1
universe:i18n@1.32.6
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
webapp@1.13.8
webapp-hashing@1.1.1
zodern:types@1.0.13
......@@ -4,11 +4,63 @@
#root {
min-height: 100vh;
}
a {
color: inherit; /* blue colors for links too */
text-decoration: inherit; /* no underline */
text-decoration: inherit;
}
div p a {
color: blue;
text-decoration: underline;
}
.ql-editor {
max-height: 600px;
}
.no-overflow {
width: inherit;
display: block;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
max-width: 100%;
overflow-wrap: anywhere;
}
.import-bookmark-cell {
white-space: nowrap;
max-width: 350px;
overflow: hidden;
text-overflow: ellipsis;
}
.hidden {
display: none;
}
.flex {
display: flex;
}
.flex.align-center {
align-items: center;
}
.myTooltipClass * {
background-color: #f9f9fd;
}
.myTooltipClassDark * {
background-color: #383838;
}
.multiline-ellipsis {
display: block;
display: -webkit-box;
-webkit-line-clamp: 4;
line-clamp: 4;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
import { Mongo } from 'meteor/mongo';
import SimpleSchema from 'simpl-schema';
import moment from 'moment';
import { getLabel } from '../utils';
const AnalyticsEvents = new Mongo.Collection('analytics');
// Deny all client-side updates since we will be using methods to manage this collection
AnalyticsEvents.deny({
insert() {
return true;
},
update() {
return true;
},
remove() {
return true;
},
});
AnalyticsEvents.targets = {
SERVICE: 'SERVICE',
WIDGET: 'WIDGET',
};
AnalyticsEvents.schema = new SimpleSchema({
target: {
type: String,
allowedValues: Object.keys(AnalyticsEvents.targets),
label: getLabel('api.analytics.labels.target'),
index: true,
},
content: {
type: String,
optional: true,
label: getLabel('api.analytics.labels.content'),
index: true,
},
count: {
type: Number,
label: getLabel('api.analytics.labels.count'),
defaultValue: 0,
},
structureId: {
type: String,
label: getLabel('api.analytics.labels.structure'),
optional: true,
index: true,
},
createdAt: {
type: Date,
label: getLabel('api.analytics.labels.createdAt'),
index: true,
autoValue() {
if (this.isUpsert) {
return new Date(moment().startOf('hour').format());
}
return this.value;
},
},
});
if (Meteor.isServer) {
AnalyticsEvents.createIndex({
structureId: 1,
content: 1,
target: 1,
createdAt: 1,
});
}
AnalyticsEvents.publicFields = {
count: 1,
structureId: 1,
content: 1,
target: 1,
createdAt: 1,
};
AnalyticsEvents.attachSchema(AnalyticsEvents.schema);
export default AnalyticsEvents;
import { Meteor } from 'meteor/meteor';
import Countly from 'countly-sdk-web';
import AnalyticsEvents from './analyticsEvents';
import Structures from '../structures/structures';
const enabledAnalytics = !Meteor.settings.public.disabledFeatures?.analytics;
const { countly } = Meteor.settings.public;
let connectionInfo = null;
let upperStructureName = null;
const trackCountly = async (options) => {
const { target, content } = options;
const user = Meteor.user();
const structure = user?.structure ? Structures.findOne(user.structure) : null;
await new Promise((resolve) => {
if (connectionInfo) return resolve();
return Meteor.call('analytics.getConnectionInfo', (error, { connection, upperStructure }) => {
if (error) {
console.error(error);
return;
}
connectionInfo = connection;
upperStructureName = upperStructure;
resolve();
});
});
Countly.q.push([
'add_event',
{
key: content,
count: 1,
segmentation: {
target,
fromPage: window.location.toString().replace(Meteor.absoluteUrl(), '/'),
isLoggedIn: !!user,
username: user?.username || null,
structure: structure?.name || null,
structureId: structure?._id || null,
upperStructure: upperStructureName || null,
connectionId: connectionInfo?._id || null,
ipAddr: connectionInfo?.ipAddr || null,
userAgent: window.navigator.userAgent || null,
// isFramed: IS_FRAMED,
},
},
]);
};
export const eventTracking = (options) => {
if (countly?.url) {
trackCountly(options);
}
if (!enabledAnalytics) {
return;
}
Meteor.call('analytics.createAnalyticsEvents', { ...options });
};
if (enabledAnalytics) {
let openedWidget = false;
const receiveMessageFromIframe = ({ data }) => {
if (data === 'openWidget' && !openedWidget) {
openedWidget = true;
eventTracking({
target: AnalyticsEvents.targets.WIDGET,
content: 'Opening widget',
});
}
};
window.addEventListener('message', receiveMessageFromIframe, false);
}
import { Factory } from 'meteor/dburles:factory';
import { Random } from 'meteor/random';
// eslint-disable-next-line import/no-extraneous-dependencies
import { faker } from '@faker-js/faker';
import AnalyticsEvents from '../analyticsEvents';
Factory.define('analytic', AnalyticsEvents, {
eventTypes: AnalyticsEvents.types.CLICK,
target: faker.lorem.word,
sessionId: () => Random.id(),
userId: () => Random.id(),
});
import { Meteor } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { _ } from 'meteor/underscore';
import { UserStatus } from 'meteor/mizzao:user-status';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { SyncedCron } from 'meteor/littledata:synced-cron';
import moment from 'moment';
import SimpleSchema from 'simpl-schema';
import AnalyticsEvents from '../analyticsEvents';
import Structures from '../../structures/structures';
export const getConnectionInfo = new ValidatedMethod({
name: 'analytics.getConnectionInfo',
validate: null,
run() {
const connection = UserStatus.connections.findOne({ _id: this.connection.id });
const user = Meteor.user({ structure: 1 });
const userStructureId = user?.structure || null;
const structure = userStructureId
? Structures.findOne({ _id: userStructureId }, { name: 1, ancestorsIds: 1 })
: null;
const upperStructure = structure
? structure.ancestorsIds.length
? Structures.findOne({ _id: structure.ancestorsIds[0] }, { name: 1 })
: structure
: null;
return { connection, upperStructure: upperStructure?.name || null };
},
});
export const createAnalyticsEvents = new ValidatedMethod({
name: 'analytics.createAnalyticsEvents',
validate: AnalyticsEvents.schema.omit('count', 'structureId', 'createdAt').validator(),
run(data) {
const structure = this.userId ? Meteor.users.findOne({ _id: this.userId }).structure : null;
return AnalyticsEvents.upsert(
{
structureId: structure,
target: data.target,
content: data.content,
createdAt: new Date(moment().startOf('hour').format()),
},
{
$inc: {
count: 1,
},
},
);
},
});
export const analyticsConnectionsCounts = new ValidatedMethod({
name: 'analytics.connections.counts',
validate: new SimpleSchema({
structureId: {
type: String,
optional: true,
},
}).validator(),
run({ structureId }) {
const additionalRequest = structureId ? { structure: structureId } : {};
const connectedUsers = Meteor.users
.find({ $or: [{ 'status.online': true }, { 'status.idle': true }] }, { fields: { structure: 1 } })
.fetch();
const structuresWithLoggedUsers = Structures.find(
{
_id: {
$in: connectedUsers.map(({ structure }) => structure),
},
},
{ fields: { _id: 1, name: 1 } },
).fetch();
const notLoggedCount = UserStatus.connections.find({ userId: { $exists: false } }).count() - 1;
const subStructuresIds = Structures.find({ ancestorsIds: structureId }, { fields: { _id: 1 } })
.fetch()
.map((s) => s._id);
if (structureId) {
subStructuresIds.push(structureId);
}
const idle = Meteor.users.find({ ...additionalRequest, 'status.online': true, 'status.idle': true }).count();
const idleWithSubStructures = Meteor.users
.find({ structure: { $in: subStructuresIds }, 'status.online': true, 'status.idle': true })
.count();
const active = Meteor.users.find({ ...additionalRequest, 'status.online': true, 'status.idle': false }).count();
const activeWithSubStructures = Meteor.users
.find({ structure: { $in: subStructuresIds }, 'status.online': true, 'status.idle': false })
.count();
const result = {
logged: connectedUsers.length,
notLogged: notLoggedCount < 0 ? 0 : notLoggedCount,
idle,
idleWithSubStructures,
active,
activeWithSubStructures,
newAccounts: Meteor.users
.find({ ...additionalRequest, createdAt: { $gte: new Date(moment().subtract(2, 'days').format()) } })
.count(),
structures: structuresWithLoggedUsers.map(({ _id, name }) => ({
name,
_id,
value: connectedUsers.filter(({ structure }) => structure === _id).length,
})),
};
return result;
},
});
const structureIdAndDateRangeSchema = {
structureId: {
type: String,
optional: true,
},
dateRange: {
type: Array,
optional: true,
min: 2,
max: 2,
},
'dateRange.$': {
type: Date,
},
};
export const getActionClickedAnalyticsEvents = new ValidatedMethod({
name: 'analytics.getActionClickedAnalyticsEvents',
validate: new SimpleSchema(structureIdAndDateRangeSchema).validator(),
run({ structureId, dateRange }) {
const structure = Structures.findOne({ _id: structureId });
const structureArrayWithChilds = [structureId, ...(structure?.childrenIds || [])];
const $match = structure ? { structureId: { $in: structureArrayWithChilds } } : {};
if (dateRange) {
$match.createdAt = {
$gte: new Date(dateRange[0]),
$lte: new Date(dateRange[1]),
};
}
const data = AnalyticsEvents.aggregate([
{ $match },
{
$group: {
_id: '$content',
count: {
$sum: {
$cond: {
if: {
$eq: ['$structureId', null],
},
then: 0,
else: '$count',
},
},
},
countNotConnected: {
$sum: {
$cond: {
if: {
$eq: ['$structureId', null],
},
then: '$count',
else: 0,
},
},
},
target: { $first: '$target' },
content: { $first: '$content' },
},
},
]);
return data;
},
});
export const getExportableAnalyticsData = new ValidatedMethod({
name: 'analytics.getExportableAnalyticsData',
validate: new SimpleSchema(structureIdAndDateRangeSchema).validator(),
run({ structureId, dateRange }) {
// const structure = structureId ? Structures.findOne({ _id: structureId }) : null;
// const structureArrayWithChilds = [structureId, ...(structure?.childrenIds || [])];
const $match = structureId ? { $or: [{ _id: structureId }, { ancestorsIds: structureId }] } : {};
const pipeline = [
{ $match },
{
$lookup: {
from: 'analytics',
// localField: '_id',
// foreignField: 'structureId',
as: 'analytics',
let: { structId: '$_id' },
pipeline: [
{
$match: {
$expr: {
$and: [
{ $eq: ['$structureId', '$$structId'] },
{ $gte: ['$createdAt', new Date(dateRange[0])] },
{ $lte: ['$createdAt', new Date(dateRange[1])] },
],
},
},
},
],
},
},
{ $unwind: '$analytics' },
{
$project: {
_id: 0,
name: 1,
count: '$analytics.count',
target: '$analytics.target',
content: '$analytics.content',
at: '$analytics.createdAt',
},
},
{
$sort: {
at: -1,
name: 1,
count: 1,
target: 1,
content: 1,
},
},
];
const data = Structures.aggregate(pipeline);
return data;
},
});
export const analyticsChartData = new ValidatedMethod({
name: 'analytics.chartdata',
validate: new SimpleSchema({
structureId: {
type: String,
optional: true,
},
content: {
type: String,
},
target: {
type: String,
},
}).validator(),
run({ structureId, content, target }) {
const structure = Structures.findOne({ _id: structureId });
const structureArrayWithChilds = [structureId, ...(structure?.childrenIds || [])];
const $facet = {};
const timeslots = (Meteor.settings.public.analyticsExpirationInDays || 30) * 24;
const now = moment().startOf('hour');
new Array(timeslots).fill().forEach(() => {
const date = now.format();
const $match = {
content,
target,
createdAt: new Date(date),
};
if (structureId) {
$match.structureId = { $in: structureArrayWithChilds };
}
$facet[date] = [
{
$match,
},
{
$group: {
_id: '$createdAt',
count: {
$sum: {
$cond: {
if: {
$eq: ['$structureId', null],
},
then: 0,
else: '$count',
},
},
},
countNotConnected: {
$sum: {
$cond: {
if: {
$eq: ['$structureId', null],
},
then: '$count',
else: 0,
},
},
},
},
},
];
now.subtract(1, 'hours');
});
const data = AnalyticsEvents.aggregate([{ $facet }])[0];
return Object.keys(data)
.map((key) => {
return {
count: data[key][0]?.count || 0,
countNotConnected: data[key][0]?.countNotConnected || 0,
slot: moment(key).format('dd - HH:mm'),
};
})
.reverse();
},
});
// Get list of all method names on Helps
const LISTS_METHODS = _.pluck([createAnalyticsEvents, getActionClickedAnalyticsEvents], 'name');
// Only allow 5 list operations per connection per second
DDPRateLimiter.addRule(
{
name(name) {
return _.contains(LISTS_METHODS, name);
},
// Rate limit per connection ID
connectionId() {
return true;
},
},
5,
1000,
);
if (Meteor.settings.public.analyticsExpirationInDays) {
SyncedCron.add({
name: 'Delete analytics old data',
schedule: function removeSchedule(parser) {
return parser.text(`every day at 2:00 am`);
},
job: function removeOldData() {
return AnalyticsEvents.remove({
createdAt: {
$lt: new Date(moment().subtract(Meteor.settings.public.analyticsExpirationInDays, 'days').format()),
},
});
},
});
}