Parcourir la source

Merge branch 'release/v0.2.0'

rigwild il y a 5 ans
Parent
commit
3ad88b79eb

+ 1 - 1
.eslintrc.js

@@ -92,7 +92,7 @@ module.exports = {
     'no-redeclare': 2,
     'no-script-url': 2,
     'no-self-compare': 2,
-    'no-sequences': 2,
+    'no-sequences': 0,
     'no-throw-literal': 2,
     'no-void': 2,
     'no-warning-comments': [0, { terms: ['todo', 'fixme'], location: 'start' }],

+ 1 - 0
.gitignore

@@ -26,3 +26,4 @@ yarn-error.log*
 /test/images
 /data/*
 /doc
+/experimentConfig/

+ 18 - 1
README.md

@@ -8,7 +8,7 @@ cd Antoine_Internship
 ```
 
 ## Run as a Docker instance
-### Configure
+### Configure application
 Use the following environment variables to configure the application.
 
 | Option      | Default value | Description | Server | Client |
@@ -35,6 +35,16 @@ Configure more deeply the way the app works by modifying *[config.js](config.js)
 | `wsLogger` | Logs : `logs/ws.log` Errors : `logs/ws.error.log` | WebSocket logger configuration |
 | `dbLogger` | Logs : `logs/db.log` Errors : `logs/db.error.log` | Database logger configuration |
 
+### Configure experiments
+If you want to use the default experiments configurations, you can skip this part, the Dockerfile does it for you.
+
+You can modify the configuration for each mixins, experiments or scene. To do so, you first need to initialize experiment configurations first.
+```sh
+cp -r experimentConfig.default experimentConfig
+```
+You can modify the configuration for each mixins, experiments or scene by modifying files in the newly created *`./experimentConfig`* directory.
+You configuration will be copied in the Docker instance.
+
 ### Run server + client
 Linux
 ```sh
@@ -109,6 +119,13 @@ yarn run server:lint
 
 
 ### Client
+To use the client, you need to initialize experiment configurations first.
+```sh
+cp -r experimentConfig.default experimentConfig
+```
+You can modify the configuration for each mixins, experiments or scene by modifying files in the newly created *`./experimentConfig`* directory.
+
+
 #### Compile and minify for production
 Files will be built to the `dist/` directory.
 ```sh

+ 10 - 1
back.Dockerfile

@@ -8,7 +8,16 @@ WORKDIR /usr/src/app
 # Server port
 EXPOSE 5000
 
+# Install dependencies and generate documentation
 RUN yarn install && yarn doc
 
 # Build front if SERVE_CLIENT=true
-CMD if [ "$SERVE_CLIENT" == "true" ] ; then NODE_ENV=test yarn test && yarn run app:build && yarn run server:start ; else NODE_ENV=test yarn test && yarn run server:start ; fi
+CMD if [ "$SERVE_CLIENT" == "true" ] ; \
+  then \
+    ([ -d ./experimentConfig ] && \
+      echo "Experiment configuration found" \
+      || echo "Experiment configuration not found, copying default" && cp -r experimentConfig.default experimentConfig) && \
+    NODE_ENV=test yarn test && \
+    yarn run app:build && \
+    yarn run server:start ; \
+  else NODE_ENV=test yarn test && yarn run server:start ; fi

+ 6 - 1
config.messagesId.js

@@ -1,5 +1,10 @@
+'use strict'
+
 // List of IDs for messages sent using WebSockets
 
+// Message ID for data collection
+export const COLLECT_DATA = 'COLLECT_DATA'
+
 // Message IDs for experiments events
 export const EXPERIMENT = {
   // An experiment was started
@@ -12,4 +17,4 @@ export const EXPERIMENT = {
   VALIDATED: 'EXPERIMENT_VALIDATED'
 }
 
-export default { EXPERIMENT }
+export default { COLLECT_DATA, EXPERIMENT }

+ 9 - 0
experimentConfig.default/Experiments/WithReference.js

@@ -0,0 +1,9 @@
+import { buildConfig } from '@/functions'
+
+// This will apply to all the scenes
+export const defaultConfig = {}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = {}
+
+export default buildConfig(defaultConfig, scenesConfig)

+ 9 - 0
experimentConfig.default/mixins/ExperimentBase.js

@@ -0,0 +1,9 @@
+import { buildConfig } from '@/functions'
+
+// This will apply to all the scenes
+export const defaultConfig = {}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = {}
+
+export default buildConfig(defaultConfig, scenesConfig)

+ 25 - 0
experimentConfig.default/mixins/ExperimentBaseExtracts.js

@@ -0,0 +1,25 @@
+import { buildConfig } from '@/functions'
+
+// This will apply to all the scenes
+export const defaultConfig = {
+  // showHoverBorder: false,
+  // extractConfig: {
+  //   x: 4,
+  //   y: 6
+  // },
+  // lockConfig: true
+}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = {
+  // bathroom: {
+  //   showHoverBorder: false,
+  //   extractConfig: {
+  //     x: 4,
+  //     y: 10
+  //   },
+  //   lockConfig: false
+  // }
+}
+
+export default buildConfig(defaultConfig, scenesConfig)

+ 11 - 8
package.json

@@ -1,6 +1,6 @@
 {
   "name": "expe-web",
-  "version": "0.1.10",
+  "version": "0.2.0",
   "private": true,
   "scripts": {
     "server:start": "node -r esm server/index.js",
@@ -13,6 +13,7 @@
   },
   "dependencies": {
     "@hapi/boom": "^7.4.2",
+    "body-parser": "^1.19.0",
     "compression": "^1.7.4",
     "core-js": "^2.6.5",
     "cors": "^2.8.5",
@@ -23,12 +24,7 @@
     "morgan": "^1.9.1",
     "serve-static": "^1.13.2",
     "sharp": "^0.22.1",
-    "vue": "^2.6.10",
-    "vue-native-websocket": "^2.0.13",
-    "vue-router": "^3.0.6",
-    "vuetify": "^1.5.14",
-    "vuex": "^3.1.0",
-    "vuex-persist": "^2.0.0",
+    "ua-parser-js": "^0.7.19",
     "winston": "^3.2.1",
     "ws": "^7.0.0"
   },
@@ -40,6 +36,7 @@
     "apidoc": "^0.17.7",
     "ava": "^1.4.1",
     "babel-eslint": "^10.0.1",
+    "deepmerge": "^3.2.0",
     "eslint": "^5.16.0",
     "eslint-plugin-vue": "^5.2.2",
     "fs-extra": "^7.0.1",
@@ -47,9 +44,15 @@
     "stylus": "^0.54.5",
     "stylus-loader": "^3.0.2",
     "supertest": "^4.0.2",
+    "vue": "^2.6.10",
     "vue-cli-plugin-vuetify": "^0.5.0",
+    "vue-native-websocket": "^2.0.13",
+    "vue-router": "^3.0.6",
     "vue-template-compiler": "^2.6.10",
-    "vuetify-loader": "^1.2.2"
+    "vuetify": "^1.5.14",
+    "vuetify-loader": "^1.2.2",
+    "vuex": "^3.1.0",
+    "vuex-persist": "^2.0.0"
   },
   "postcss": {
     "plugins": {

+ 15 - 9
server/functions.js

@@ -3,7 +3,7 @@
 import { promises as fs } from 'fs'
 import path from 'path'
 import boom from '@hapi/boom'
-import { logger, imagesPath, fileNameConvention, sceneFileNameBlackList } from '../config'
+import { logger, imagesPath, fileNameConvention, sceneFileNameBlackList, TEST_MODE } from '../config'
 
 /**
  * Call the error handler if a middleware function throw an error
@@ -13,24 +13,30 @@ import { logger, imagesPath, fileNameConvention, sceneFileNameBlackList } from '
  */
 export const asyncMiddleware = fn => (req, res, next) => {
   Promise.resolve(fn(req, res, next)).catch(err => {
-    // Check whether the error is a boom error
-    if (!err.isBoom) {
-      // The error was not recognized, send a 500 HTTP error
-      return next(boom.internal(err))
-    }
-    // It is a boom error, pass it to the error handler
     next(err)
   })
 }
 
 // Middleware to handle middleware errors
 export const errorHandler = (err, req, res, next) => {
+  // Check whether the error is a boom error
+  if (!err.isBoom) {
+    // Check if error is invalid JSON body
+    if (err instanceof SyntaxError && err.status === 400 && err.hasOwnProperty('body'))
+      err = boom.badRequest(err)
+    else {
+      // The error was not recognized, send a 500 HTTP error
+      err = boom.internal(err)
+    }
+  }
+
   const { output: { payload } } = err
 
   // Pass the error to the logging handler
   let errorLogged = new Error(`Error ${payload.statusCode} - ${payload.error} - Message :\n${payload.message}`)
   errorLogged.stack = err.stack
-  logger.error(formatError(errorLogged, err.data))
+
+  if (!TEST_MODE) logger.error(formatError(errorLogged, err.data))
 
   // Send the error to the client
   res.status(payload.statusCode).json({
@@ -51,7 +57,7 @@ export const errorHandler = (err, req, res, next) => {
  * @throws missing parameters
  */
 export const checkRequiredParameters = (requiredParameters, parameters) => {
-  if (!requiredParameters.every(aRequiredParameter => Object.keys(parameters).includes(aRequiredParameter)))
+  if (!requiredParameters.every(aRequiredParameter => parameters.hasOwnProperty(aRequiredParameter)))
     throw boom.badRequest(`Missing parameter(s). Required parameters : ${requiredParameters.join(', ')}.`)
 }
 

+ 6 - 0
server/index.js

@@ -6,6 +6,8 @@ import compression from 'compression'
 import serveStatic from 'serve-static'
 import helmet from 'helmet'
 import cors from 'cors'
+import bodyParser from 'body-parser'
+
 import routes from './routes'
 import { errorHandler, formatLog } from './functions'
 import { apiPrefix, imageServedUrl, serverPort, serveClient, imagesPath, logger } from '../config'
@@ -14,6 +16,7 @@ import connectDb from './database'
 const morgan = require('morgan')
 
 const app = express()
+app.enable('trust proxy')
 
 // Activating logging
 app.use(morgan('combined', {
@@ -29,6 +32,9 @@ app.use(helmet())
 // Turn "Cross-origin resource sharing" on to allow remote clients to connect to the API
 app.use(cors())
 
+// Parse JSON body
+app.use(bodyParser.json())
+
 // Serve images. "serve-static" is used because it caches images ("express.static" doesn't)
 app.use(imageServedUrl, serveStatic(imagesPath))
 

+ 87 - 0
server/routes/dataCollect.js

@@ -0,0 +1,87 @@
+'use strict'
+
+import express from 'express'
+import boom from '@hapi/boom'
+import userAgentParser from 'ua-parser-js'
+
+import { TEST_MODE } from '../../config'
+import { COLLECT_DATA } from '../../config.messagesId'
+import DataController from '../database/controllers/Data'
+import { asyncMiddleware, checkRequiredParameters } from '../functions'
+
+const router = express.Router()
+
+/**
+ * @api {post} /dataCollect /dataCollect
+ * @apiVersion 0.1.11
+ * @apiName dataCollect
+ * @apiGroup API
+ *
+ * @apiDescription Collect user's data
+ *
+ * @apiParam {String} uuid The unique user identifier
+ * @apiParam {Object} screen Screen data, `window.screen` @see https://developer.mozilla.org/en-US/docs/Web/API/Screen
+ *
+ * @apiExample Usage example
+ * curl -i -L -H "Content-Type: application/json" -X POST "http://diran.univ-littoral.fr/api/dataCollect" -d {"uuid":"test","screen":{"width":1920,"height":1024}}
+ *
+ * @apiSuccessExample {string} Success response example
+ * HTTP/1.1 200 OK /api/dataCollect
+ * OK
+ *
+ * @apiError (Error 4xx) 400_[1] Missing parameter(s)
+ * @apiErrorExample {json} Missing parameter
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Missing parameter(s). Required parameters : uuid, screen."
+ * }
+ *
+ * @apiError (Error 4xx) 400_[2] Invalid query parameter
+ * @apiErrorExample {json} Invalid query parameter(s)
+ * HTTP/1.1 400 Bad Request
+ * {
+ *   "message": "Invalid body parameter(s).",
+ *   "data": [
+ *     "\"uuid\" must be a string.",
+ *     "\"screen\" must be a valid object."
+ *   ]
+ * }
+ *
+ */
+
+router.post('/', asyncMiddleware(async (req, res) => {
+  // Check the request contains all the required body parameters
+  const b = req.body
+  checkRequiredParameters(['uuid', 'screen'], b)
+
+  let errorList = []
+
+  if (typeof b.uuid !== 'string')
+    errorList.push('"uuid" must be a string.')
+
+  if (typeof b.screen !== 'object' || Object.keys(b.screen).length > 30)
+    errorList.push('"screen" must be a valid object.')
+
+  // Check there is no errors with parameters
+  if (errorList.length > 0)
+    throw boom.badRequest('Invalid body parameter(s).', errorList)
+
+  const userAgent = userAgentParser(req.headers['user-agent'])
+
+  // Collected data object
+  const data = {
+    uuid: b.uuid,
+    msgId: COLLECT_DATA,
+    msg: {
+      screen: b.screen,
+      userAgent,
+      ip: req.ip
+    }
+  }
+
+  if (!TEST_MODE) await DataController.add(data)
+
+  res.send({ message: 'OK' })
+}))
+
+export default router

+ 3 - 1
server/routes/index.js

@@ -6,6 +6,7 @@ import listSceneQualities from './listSceneQualities'
 import getImage from './getImage'
 import getImageExtracts from './getImageExtracts'
 import ping from './ping'
+import dataCollect from './dataCollect'
 
 const router = express.Router()
 
@@ -13,6 +14,7 @@ router.use('/listScenes', listScenes)
 router.use('/listSceneQualities', listSceneQualities)
 router.use('/getImage', getImage)
 router.use('/getImageExtracts', getImageExtracts)
-router.get('/ping', ping)
+router.use('/dataCollect', dataCollect)
+router.use('/ping', ping)
 
 export default router

+ 3 - 5
server/routes/listScenes.js

@@ -45,11 +45,9 @@ const router = express.Router()
  * @returns {string[]} the list of files
  * @throws the directory does not exist or is not accessible
  */
-export const getSceneList = () => {
-  return fs.readdir(imagesPath).catch(() => {
-    throw boom.internal(`Can't access the "${path.basename(imagesPath)}" directory. Check it exists and you have read permission on it.`)
-  })
-}
+export const getSceneList = () => fs.readdir(imagesPath).catch(() => {
+  throw boom.internal(`Can't access the "${path.basename(imagesPath)}" directory. Check it exists and you have read permission on it.`)
+})
 
 // Route which returns a list of all available scenes in the `imagesPath` directory
 router.get('/', asyncMiddleware(async (req, res) => res.json({ data: await getSceneList() })))

+ 1 - 1
server/routes/ping.js

@@ -20,6 +20,6 @@ const router = express.Router()
  * pong
  */
 
-router.get('/ping', (req, res) => res.send('pong'))
+router.get('/', (req, res) => res.send('pong'))
 
 export default router

+ 1 - 4
server/webSocket/index.js

@@ -33,10 +33,7 @@ const createWsServer = httpServer => {
   wss.on('listening', () => wsLogger.info(formatLog('The WebSocket server was started')))
   wss.on('error', err => wsLogger.error(formatError(err)))
 
-  wss.on('connection', (ws, req) => {
-    // Unique identifier passed with the request url
-    ws.uuid = req.url.replace('/?uuid=', '')
-
+  wss.on('connection', ws => {
     wsLogger.info(formatLog('New client connected.'))
 
     ws.on('message', data => messageHandler(ws)(data).catch(err => errorHandler(ws)(err)))

+ 2 - 1
server/webSocket/messageHandler.js

@@ -21,8 +21,9 @@ const messageHandler = ws => async data => {
   catch (err) {
     throw new Error('Invalid JSON data.')
   }
+  if (!TEST_MODE && !json.uuid)
+    throw new Error('"uuid" was not provided.')
 
-  json.WS_UNIQUE_UUID = ws.uuid
   await DataController.add(json)
   if (!TEST_MODE) wsLogger.info(formatLog(json, 'message'))
   ws.send('{"message":"ok"}')

+ 19 - 9
src/components/ExperimentsComponents/ExtractConfiguration.vue

@@ -27,15 +27,16 @@
                 max="15"
               />
 
-              <v-btn @click="setConfig" :disabled="!isConfigNew">Confirm</v-btn>
-            </div>
-            <div v-else key="arrow">
-              <v-btn flat round @click="isExpanded = true">
-                <v-icon>keyboard_arrow_down</v-icon>
-              </v-btn>
+              <v-btn @click="setExtractConfig" :disabled="!isConfigNew">Confirm</v-btn>
             </div>
           </v-slide-y-transition>
 
+          <div>
+            <v-btn flat round @click="isExpanded = !isExpanded">
+              <v-icon class="rotated180-duration" :class="{ rotated180: isExpanded }" key="arrow-down">keyboard_arrow_down</v-icon>
+            </v-btn>
+          </div>
+
           <v-alert v-if="loadingErrorMessage" :value="true" type="error" v-text="loadingErrorMessage" />
         </v-flex>
       </v-layout>
@@ -62,7 +63,7 @@ export default {
         x: 4,
         y: 4
       },
-      // Updated when `setConfig` is called
+      // Updated when `setExtractConfig` is called
       extractConfig: {
         x: 4,
         y: 4
@@ -85,11 +86,20 @@ export default {
       this.extractConfig.x = this.experimentConfig.x
       this.extractConfig.y = this.experimentConfig.y
     },
-    setConfig() {
+    setExtractConfig() {
       this.extractConfig.x = this.experimentConfig.x
       this.extractConfig.y = this.experimentConfig.y
-      this.$emit('setConfig', this.experimentConfig)
+      this.$emit('setExtractConfig', this.experimentConfig)
     }
   }
 }
 </script>
+
+<style scoped>
+.rotated180 {
+  transform: rotate(180deg);
+}
+.rotated180-duration {
+  transition: transform .5s ease-in-out !important;
+}
+</style>

+ 15 - 0
src/functions.js

@@ -2,6 +2,8 @@ export const API_PREFIX = '/api'
 export const API_ROUTES = {
   ping: () => `${API_PREFIX}/ping`,
 
+  dataCollect: () => `${API_PREFIX}/dataCollect`,
+
   listScenes: () => `${API_PREFIX}/listScenes`,
 
   listSceneQualities: sceneName => `${API_PREFIX}/listSceneQualities?${new URLSearchParams({ sceneName })}`,
@@ -57,3 +59,16 @@ export const shuffleArray = array => {
   }
   return array
 }
+
+/**
+ * Build a configuration file by merging the default config with the asked scene.
+ * The asked scene config will overwrite the default config.
+ * @param {Object} defaultConfig The default configuration object
+ * @param {Object} scenesConfig The scenes specific configuration
+ * @returns {Function} A function that will return the scene configuration
+ */
+export const buildConfig = (defaultConfig = {}, scenesConfig = {}) =>
+  sceneName => Object.assign(defaultConfig, scenesConfig[sceneName])
+
+// Serialize non-serializable objects (like window.screen)
+export const serialize = obj => Object.keys(Object.getPrototypeOf(obj)).reduce((acc, x) => ((acc[x] = obj[x]), acc), {})

+ 27 - 0
src/mixins/ExperimentBase/config.js

@@ -0,0 +1,27 @@
+import deepmerge from 'deepmerge'
+import { buildConfig } from '@/functions'
+
+// const getMixinConfig = () => {}
+const getGlobalConfig = () => import('@/../experimentConfig/mixins/ExperimentBase')
+
+// This will apply to all the scenes
+export const defaultConfig = async () => {
+  const globalConfig = (await getGlobalConfig()).defaultConfig
+  return deepmerge.all([
+    {
+      lockConfig: true
+    },
+    globalConfig
+  ])
+}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = async () => {
+  const globalConfig = (await getGlobalConfig()).scenesConfig
+  return deepmerge.all([
+    {},
+    globalConfig
+  ])
+}
+
+export default async () => buildConfig(await defaultConfig(), await scenesConfig())

+ 13 - 2
src/mixins/ExperimentBase/index.vue

@@ -23,15 +23,18 @@ export default {
 
       loadingMessage: null,
       loadingErrorMessage: null,
-      qualities: null
+      qualities: null,
+
+      lockConfig: null
     }
   },
   computed: {
     ...mapGetters(['getHostURI', 'getExperimentProgress', 'isExperimentDone'])
   },
+
   mounted() {
     if (!this.getExperimentProgress({ experimentName: this.experimentName, sceneName: this.sceneName }).experimentName)
-      this.sendMessage({ msgId: experimentMsgId.STARTED })
+      this.sendMessage({ msgId: experimentMsgId.STARTED, experimentName: this.experimentName, sceneName: this.sceneName })
 
     // Check if the experiment is already finished
     if (this.experimentName && this.sceneName && this.isExperimentDone({ experimentName: this.experimentName, sceneName: this.sceneName })) {
@@ -39,6 +42,7 @@ export default {
       this.$router.push(`/experiments/${this.experimentName}`)
     }
   },
+
   methods: {
     ...mapActions(['setExperimentProgress', 'setExperimentDone', 'sendMessage']),
 
@@ -60,6 +64,13 @@ export default {
       // console.log('Saved data from local state to store.', this.$data)
     },
 
+    // Load a config object into the local state
+    async loadConfig(configFn) {
+      const config = (await configFn())(this.sceneName)
+      // console.log('Loaded configuration', config)
+      Object.assign(this.$data, config)
+    },
+
     // Finish an experiment, sending full data to the server
     // Don't forget to surcharge this function when using this mixin to add more data
     finishExperiment() {

+ 43 - 0
src/mixins/ExperimentBaseExtracts/config.js

@@ -0,0 +1,43 @@
+import deepmerge from 'deepmerge'
+import { buildConfig } from '@/functions'
+
+const getMixinConfig = () => import('@/mixins/ExperimentBase/config')
+const getGlobalConfig = () => import('@/../experimentConfig/mixins/ExperimentBaseExtracts')
+
+// This will apply to all the scenes
+export const defaultConfig = async () => {
+  // Import parent mixin config
+  const mixinConfig = await getMixinConfig().then(({ defaultConfig: fn }) => fn())
+
+  // Import global config
+  const globalConfig = (await getGlobalConfig()).defaultConfig
+
+  return deepmerge.all([
+    mixinConfig,
+    {
+      showHoverBorder: false,
+      extractConfig: {
+        x: 4,
+        y: 4
+      }
+    },
+    globalConfig
+  ])
+}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = async () => {
+  // Import parent mixin config
+  const mixinConfig = await getMixinConfig().then(({ scenesConfig: fn }) => fn())
+
+  // Import global config
+  const globalConfig = (await getGlobalConfig()).scenesConfig
+
+  return deepmerge.all([
+    mixinConfig,
+    {},
+    globalConfig
+  ])
+}
+
+export default async () => buildConfig(await defaultConfig(), await scenesConfig())

+ 19 - 7
src/mixins/ExperimentBaseExtracts/index.vue

@@ -17,12 +17,14 @@ export default {
   mixins: [ExperimentBase],
   data() {
     return {
-      // Updated when `setConfig` is called
+      // Updated when `setExtractConfig` is called
       extractConfig: {
-        x: 4,
-        y: 4
+        x: null,
+        y: null
       },
-      extracts: []
+      extracts: [],
+
+      showHoverBorder: null
     }
   },
   computed: {
@@ -45,8 +47,8 @@ export default {
     },
 
     // Config was updated, load extracts and save progression
-    async setConfig(config, configuratorRef) {
-      if (!config || !configuratorRef) return
+    async setExtractConfig(config, configuratorRef) {
+      if (!config) return
 
       this.loadingMessage = 'Loading configuration extracts...'
       this.loadingErrorMessage = null
@@ -64,7 +66,9 @@ export default {
           precQuality: findNearestLower(data.info.image.quality, this.qualities),
           loading: false
         }))
-        configuratorRef.setVisibility(false)
+
+        // If there is a configurator, retract it
+        if (configuratorRef) configuratorRef.setVisibility(false)
       }
       catch (err) {
         console.error('Failed to load new configuration', err)
@@ -137,3 +141,11 @@ export default {
   }
 }
 </script>
+
+<style>
+/* White border when hovering on extracts */
+ .extract-hover-border:hover {
+  z-index: 1;
+  outline: 2px #f4f4f4 solid;
+}
+</style>

+ 2 - 2
src/router/experiments.js

@@ -3,14 +3,14 @@ export default [
     path: '/experiments/ExperimentNoReference/:sceneName',
     name: 'ExperimentNoReference',
     fullName: 'No reference image',
-    component: () => import('@/views/Experiments/NoReference.vue'),
+    component: () => import('@/views/Experiments/NoReference'),
     props: true
   },
   {
     path: '/experiments/ExperimentWithReference/:sceneName',
     name: 'ExperimentWithReference',
     fullName: 'With reference image',
-    component: () => import('@/views/Experiments/WithReference.vue'),
+    component: () => import('@/views/Experiments/WithReference'),
     props: true
   }
 ]

+ 22 - 4
src/store/actions.js

@@ -1,6 +1,6 @@
 import Vue from 'vue'
 import router from '../router'
-import { API_ROUTES, buildURI, buildWsURI, delay } from '../functions'
+import { API_ROUTES, buildURI, buildWsURI, delay, serialize } from '../functions'
 
 export default {
   setGdprValidated({ state, commit }) {
@@ -18,7 +18,7 @@ export default {
     commit('resetApp', { gdprConsent, hostConfig, progression })
   },
 
-  async setHostConfig({ state, commit }, { ssl, host, port }) {
+  async setHostConfig({ state, commit, dispatch }, { ssl, host, port }) {
     // Timeout after 1s
     const controller = new AbortController()
     const signal = controller.signal
@@ -39,9 +39,11 @@ export default {
         if (!state.socket.isConnected)
           throw new Error('Could not connect to remote WebSocket server.')
 
+
         // Configuration is valid
         commit('setHostConfig', { ssl, host, port })
         router.push('/experiments')
+        dispatch('collectUserData')
       })
       .catch(err => {
         // Host not reachable or invalid HTTP status code
@@ -61,8 +63,24 @@ export default {
     else throw new Error('Could not connect to WebSocket server. Host is not configured.')
   },
 
-  sendMessage(_, { msgId, msg = undefined }) {
-    Vue.prototype.$socket.send(JSON.stringify({ msgId, msg }))
+  async collectUserData({ state, getters }) {
+    let screen = serialize(window.screen)
+    screen.orientation = serialize(window.screen.orientation)
+
+    return fetch(getters.getHostURI + API_ROUTES.dataCollect(), {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        uuid: state.uuid,
+        screen
+      })
+    })
+  },
+
+  sendMessage({ state }, { msgId, msg = undefined }) {
+    Vue.prototype.$socket.send(JSON.stringify({ uuid: state.uuid, msgId, msg }))
   },
 
   async loadScenesList({ getters: { isHostConfigured, getHostURI }, commit }) {

src/views/Experiments/NoReference.vue → src/views/Experiments/NoReference/index.vue


+ 37 - 0
src/views/Experiments/WithReference/config.js

@@ -0,0 +1,37 @@
+import deepmerge from 'deepmerge'
+import { buildConfig } from '@/functions'
+
+const getMixinConfig = () => import('@/mixins/ExperimentBaseExtracts/config')
+const getGlobalConfig = () => import('@/../experimentConfig/Experiments/WithReference')
+
+// This will apply to all the scenes
+export const defaultConfig = async () => {
+  // Import parent mixin config
+  const mixinConfig = await getMixinConfig().then(({ defaultConfig: fn }) => fn())
+
+  // Import global config
+  const globalConfig = (await getGlobalConfig()).defaultConfig
+
+  return deepmerge.all([
+    mixinConfig,
+    {},
+    globalConfig
+  ])
+}
+
+// This will overwrite the config for the given scene
+export const scenesConfig = async () => {
+  // Import parent mixin config
+  const mixinConfig = await getMixinConfig().then(({ scenesConfig: fn }) => fn())
+
+  // Import global config
+  const globalConfig = (await getGlobalConfig()).scenesConfig
+
+  return deepmerge.all([
+    mixinConfig,
+    {},
+    globalConfig
+  ])
+}
+
+export default async () => buildConfig(await defaultConfig(), await scenesConfig())

+ 10 - 11
src/views/Experiments/WithReference.vue

@@ -12,7 +12,7 @@
 
           <h1>Experiment with reference - {{ sceneName }}</h1>
           <!-- Extract configuration -->
-          <extract-configuration @setConfig="setConfig($event, $refs.configurator)" :loading-error-message="loadingErrorMessage" ref="configurator" />
+          <extract-configuration v-if="lockConfig === false" @setExtractConfig="setExtractConfig($event, $refs.configurator)" :loading-error-message="loadingErrorMessage" ref="configurator" />
           <!--/ Extract configuration -->
         </v-flex>
         <!-- Loading screen -->
@@ -48,7 +48,8 @@
                           :src="anExtract.link"
                           @click.left.prevent="extractAction($event, anExtract)"
                           @click.right.prevent="extractAction($event, anExtract)"
-                          class="cursor extract"
+                          class="cursor"
+                          :class="{ 'extract-hover-border': showHoverBorder === true }"
                         >
                           <template v-slot:placeholder>
                             <v-layout
@@ -91,6 +92,7 @@ import ExperimentBaseExtracts from '@/mixins/ExperimentBaseExtracts'
 import { API_ROUTES } from '@/functions'
 import Loader from '@/components/Loader.vue'
 import ExtractConfiguration from '@/components/ExperimentsComponents/ExtractConfiguration.vue'
+import experimentConfig from './config'
 
 export default {
   name: 'ExperimentWithReference',
@@ -108,6 +110,9 @@ export default {
   },
 
   async mounted() {
+    // Load config for this scene to local state
+    await this.loadConfig(experimentConfig)
+
     // Load progress from store into local state
     this.loadProgress()
 
@@ -118,13 +123,14 @@ export default {
     ])
 
     // Load the cached configuration in the configurator component
-    this.$refs.configurator.setDefaultConfig(this.extractConfig)
+    if (this.lockConfig === false) this.$refs.configurator.setDefaultConfig(this.extractConfig)
 
     // Load extracts of none were cached
-    if (this.extracts.length === 0) await this.setConfig(this.extractConfig, this.$refs.configurator)
+    if (this.extracts.length === 0) await this.setExtractConfig(this.extractConfig, this.$refs.configurator)
 
     this.saveProgress()
   },
+
   methods: {
     // Load the reference image from the API
     async getReferenceImage() {
@@ -138,10 +144,3 @@ export default {
   }
 }
 </script>
-
-<style scoped>
-/* White border when hovering on extracts
- .extract:hover {
-  outline: 2px #f4f4f4 solid;
-} */
-</style>

Fichier diff supprimé car celui-ci est trop grand
+ 129 - 7
src/views/GdprNotice.vue


+ 4 - 6
test/api/_test_functions.js

@@ -2,11 +2,13 @@
 
 import path from 'path'
 import express from 'express'
+import bodyParser from 'body-parser'
 import WebSocket from 'ws'
 import serveStatic from 'serve-static'
 import routes from '../../server/routes'
 import { apiPrefix, imageServedUrl, imagesPath } from '../../config'
 import connectDb from '../../server/database'
+import { errorHandler } from '../../server/functions'
 import { errorHandler as wsErrorHandler } from '../../server/webSocket'
 import wsMessageHandler from '../../server/webSocket/messageHandler'
 
@@ -29,14 +31,10 @@ export const json = obj => 'JSON DATA : ' + (JSON.stringify(obj, null, 2) || obj
  */
 export const getHttpServer = () => {
   const app = express()
+  app.use(bodyParser.json())
   app.use(imageServedUrl, serveStatic(imagesPath))
   app.use(apiPrefix, routes)
-  app.use((err, req, res, next) => {
-    res.status(err.output.payload.statusCode).json({
-      message: err.message || err.output.payload.message,
-      data: err.data || undefined
-    })
-  })
+  app.use(errorHandler)
   return app
 }
 

+ 41 - 0
test/api/dataCollect.js

@@ -0,0 +1,41 @@
+'use strict'
+
+import test from 'ava'
+import request from 'supertest'
+import { apiPrefix } from '../../config'
+import { json, getHttpServer } from './_test_functions'
+
+// ROUTE /dataCollect
+
+// Before each tests, start a server
+test.beforeEach(async t => (t.context.server = await getHttpServer()))
+
+test('POST /dataCollect - No body', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/dataCollect`)
+
+  t.is(res.status, 400, json(res))
+  t.true(res.body.message.includes('Missing parameter'), json(res.body))
+  t.true(res.body.message.includes('uuid'), json(res.body))
+  t.true(res.body.message.includes('screen'), json(res.body))
+})
+
+test('POST /dataCollect - Invalid body parameters', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/dataCollect`)
+    .send({ uuid: 42, screen: 'not an object' })
+
+  t.is(res.status, 400, json(res))
+  t.true(res.body.message.includes('Invalid body parameter'), json(res.body))
+  t.truthy(res.body.data.find(x => x.includes('"uuid" must be a string.')), json(res.body))
+  t.truthy(res.body.data.find(x => x.includes('"screen" must be a valid object.')), json(res.body))
+})
+
+test('POST /dataCollect - Valid body parameters', async t => {
+  const res = await request(t.context.server)
+    .post(`${apiPrefix}/dataCollect`)
+    .send({ uuid: 'test', screen: { width: 1920, height: 1080 } })
+
+  t.is(res.status, 200, json(res))
+  t.is(res.body.message, 'OK', json(res.body))
+})

+ 66 - 4
yarn.lock

@@ -1828,6 +1828,22 @@ body-parser@1.18.3:
     raw-body "2.3.3"
     type-is "~1.6.16"
 
+body-parser@^1.19.0:
+  version "1.19.0"
+  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
+  integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+  dependencies:
+    bytes "3.1.0"
+    content-type "~1.0.4"
+    debug "2.6.9"
+    depd "~1.1.2"
+    http-errors "1.7.2"
+    iconv-lite "0.4.24"
+    on-finished "~2.3.0"
+    qs "6.7.0"
+    raw-body "2.4.0"
+    type-is "~1.6.17"
+
 bonjour@^3.5.0:
   version "3.5.0"
   resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@@ -2012,6 +2028,11 @@ bytes@3.0.0:
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
 
+bytes@3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
+  integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+
 cacache@^10.0.4:
   version "10.0.4"
   resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460"
@@ -3122,6 +3143,11 @@ deepmerge@^1.5.2:
   resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
   integrity sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==
 
+deepmerge@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e"
+  integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==
+
 default-gateway@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b"
@@ -4840,6 +4866,17 @@ http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
     setprototypeof "1.1.0"
     statuses ">= 1.4.0 < 2"
 
+http-errors@1.7.2:
+  version "1.7.2"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
+  integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.1"
+    statuses ">= 1.5.0 < 2"
+    toidentifier "1.0.0"
+
 http-parser-js@>=0.4.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8"
@@ -4885,7 +4922,7 @@ iconv-lite@0.4.23:
   dependencies:
     safer-buffer ">= 2.1.2 < 3"
 
-iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
+iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -7695,7 +7732,7 @@ qs@6.5.2, qs@~6.5.2:
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
   integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
 
-qs@^6.5.1:
+qs@6.7.0, qs@^6.5.1:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
   integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
@@ -7759,6 +7796,16 @@ raw-body@2.3.3:
     iconv-lite "0.4.23"
     unpipe "1.0.0"
 
+raw-body@2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
+  integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+  dependencies:
+    bytes "3.1.0"
+    http-errors "1.7.2"
+    iconv-lite "0.4.24"
+    unpipe "1.0.0"
+
 rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
@@ -8377,6 +8424,11 @@ setprototypeof@1.1.0:
   resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
   integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
 
+setprototypeof@1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
+  integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+
 sha.js@^2.4.0, sha.js@^2.4.8:
   version "2.4.11"
   resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
@@ -8727,7 +8779,7 @@ static-extend@^0.1.1:
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.4.0 < 2":
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
@@ -9217,6 +9269,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
+toidentifier@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
+  integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+
 topo@3.x.x:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/topo/-/topo-3.0.3.tgz#d5a67fb2e69307ebeeb08402ec2a2a6f5f7ad95c"
@@ -9309,7 +9366,7 @@ type-fest@^0.4.1:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8"
   integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw==
 
-type-is@~1.6.16:
+type-is@~1.6.16, type-is@~1.6.17:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
   integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -9322,6 +9379,11 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
+ua-parser-js@^0.7.19:
+  version "0.7.19"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
+  integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
+
 uc.micro@^1.0.1, uc.micro@^1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"