-
Notifications
You must be signed in to change notification settings - Fork 8
NewTask
These are some very preliminary notes about how to add a new task to the system. Feel free to improve them!
As an example scenario, let's suppose we want to create a new task where participants take their temperature with a Bluetooth sensor and send the temperature to the server. Adding a new task means adding functionalities to the 3 modules of the system: API, Web and App.
First, it's good to let other people know what kind of data you want to send. Create an example data type inside the folder "models", something like:
{
"userKey":"asdasd",
"studyKey":"asdasd",
"taskId":2,
"createdTS": "2019-02-27T12:46:07.294Z",
"temperature": 36.3
}
Also the study description will have to include this type of task.
Add the task configuration as an example in the models
folder in the study_description
.
As an example:
{
"id":4,
"type":"temperature",
"scheduling":{
"startEvent":"consent",
"startDelaySecs":1000,
"untilSecs":100000000,
"intervalType":"d",
"interval":3,
"months":[ ],
"monthDays":[ ],
"weekDays":[ ]
},
"allowManual": true,
}
Here I am assuming that the task has a flag that allows users to specify the temperature manually if they want.
Then you need to add support on the DB. Create a new file in src/DB to support saving and searching of your temperatures:
export default async function (db, logger) {
let collection = await utils.getCollection(db, 'temperatures')
return {
async getAllTemperatures () {
var query = 'FOR temperature in temperatures RETURN temperature'
applogger.trace('Querying "' + query + '"')
let cursor = await DAO.query(query)
return cursor.all()
},
async getAllTemperaturesByUser (userKey) {
var query = 'FOR temperature in temperatures FILTER data.userKey == @userKey RETURN temperature'
let bindings = { userKey: userKey }
applogger.trace(bindings, 'Querying "' + query + '"')
let cursor = await db.query(query, bindings)
return cursor.all()
},
async createTemperature (newtemperature) {
let meta = await collection.save(newtemperature)
newtemperature._key = meta._key
return newtemperature
},
async deleteTemperature (_key) {
await collection.remove(_key)
return true
}
}
}
I am omitting the imports here for brevity.
This is the bare minimum, you may need more functions depending on the specific use cases. Also, you need to add these functions to the DB object, check the DB.mjs file to see how.
Once the database part is sorted, you can create an endpoint where to save and retrieve these data. Add a file
in the "routes" folder, we can call it temperatures.mjs
.
export default async function () {
router.get('/temperatures', passport.authenticate('jwt', { session: false }), async function (req, res) {
try {
// some access control is needed here, check examples from other data types
let temperatures = await DAO.getAllTemperatures()
res.send(temperatures)
} catch (err) {
applogger.error({ error: err }, 'Cannot retrieve temperatures')
res.sendStatus(500)
}
})
router.post('/temperatures', passport.authenticate('jwt', { session: false }), async function (req, res) {
let newtemperature = req.body
// skipping access control here
newtemperature.userKey = req.user._key
try {
newtemperature = await DAO.createTemperatures(newtemperature)
// also update task status
let participant = await DAO.getParticipantByUserKey(req.user._key)
if (!participant) return res.sendStatus(404)
let study = participant.studies.find((s) => {
return s.studyKey === newanswer.studyKey
})
if (!study) return res.sendStatus(400)
let taskItem = study.taskItemsConsent.find(ti => ti.taskId === newtemperature.taskId)
if (!taskItem) return res.sendStatus(400)
taskItem.lastExecuted = newtemperature.createdTS
// update the participant
await DAO.replaceParticipant(participant._key, participant)
res.send(newtemperature)
applogger.info({ userKey: req.user._key, taskId: newtemperature.taskId, studyKey: newtemperature.studyKey }, 'Participant has sent new temperature')
auditLogger.log('temperatureCreated', req.user._key, newtemperature.studyKey, newtemperature.taskId, 'Temperature created by participant with key ' + participant._key + ' for study with key ' + newtemperature.studyKey, { temperatureKey: newtemperature._key })
} catch (err) {
applogger.error({ error: err }, 'Cannot store new temperature')
res.sendStatus(500)
}
})
return router
}
This also needs to be added with the other routes, see app.mjs. You will have to add something like:
import TemperatureRouter from './routes/temperatures.mjs'
...
app.use(api_prefix, await TemperatureRouter())
Let's not forget when deleting a user or a study! The related data should be deleted as well.
See the delete()
functions in routes/studies.mjs and routes/participants.mjs and add the deletion for the temperature also there.
For example when deleting participants:
// Remove temperatures
let temperatures = await DAO.getAllTemperaturesByUser(userKey)
for (let i = 0; i < temperatures.length; i++) {
let tempKey = temperatures[i]._key
await DAO.deleteTemperature(tempKey)
}
You need to add the possibility for the researcher to actually add that task description.
Check inside src/components/StudyDesignTasks.vue
, where it says <!-- dropdown content -->
The "add" button needs to add a task in the study description:
<q-item clickable v-close-popup @click.native="addTempT()">
<q-item-section>
<q-item-label>Measure temperature Task</q-item-label>
</q-item-section>
</q-item>
The addTempT()
needs to be implemented in the methods section of the component:
addTempT () {
this.value.tasks.push({
id: this.value.tasks.length + 1,
type: 'temperature',
scheduling: {
// Fill this in. See example from other tasks!
},
allowManual: false
})
this.update()
},
You also need to add the configuration interface on the same page. We can add:
<div v-if="task.type === 'temperature'" class="row">
<div class="col-4">
<div class="text-bold">
Allow manual input:
</div>
<div class="text-caption">
If set, the user can enter the temperature manually.
</div>
</div>
<div class="col q-pl-sm">
<q-checkbox v-model="task.allowManual" @input="update()"/>
</div>
</div>
This page should be set now.
But, remember that each task generates a consent item!
Go and modify src/components/StudyDesignConsent.vue
.
In created()
:
TBD
then in generatePrivacy()
:
TBD
As you can see, we also need some text to be added to the i18n objects, in this case:
privacyPolicy.collectedDataTemperature
and consent.taskItemTemperature
which needs to have a correspondence in the translation files:
collectedDataTemperature: `\u2022 Body temperature.`,
...
taskItemTemperature: `I agree to provide my temperature, {scheduling}`
This should be all in the web. Don't forget to test the whole thing ! You can produce a new study description and check that the object that is stored in Arango is the correct one.
The app is where the task will likely need more code to be developed. You can follow these steps:
You need to send your data to the server.
Open src/modules/API.js
and add the endpoint you need for your temperature:
// send temperature to server
sendTemperature: async function (temperature) {
return axios.post(BASE_URL + '/temperatures', temperature, axiosConfig)
}
It is also a good idea to have a mock version of it so you can test the task without
having a running server.
Simply add this to the API.mock.js
:
sendTemperature: async function (temperature) {
console.log('API - sending temperature', temperature)
return true
}
If you need to add a new Cordova plugin for supporting it, please do so inside
the cordova folder.
Check the available functionalities before doing this! See the modules
folder
and also the phone.js
object. Maybe what you need is already there.
In our example, we are supposing to use the Bluetooth Low Energy plugin.
Now, it is a good idea to develop the Bluetooth protocol in a separate module.
Let's create a file inside /src/modules
called thermometer.js
.
I am not going to write the full code here, but a draft skeleton could work like this:
export default {
async findSensor (cbk) {
// scans the bluetooth and calls cbk when a sensor is found
},
async getLoggedTemperatures (sensor) {
// connects to a specific sensor and retrieves all logged temperatures
},
async registerToLiveTemperatures (sensor) {
// connects to a sensor and receives live temperatures
},
async unregisterToLiveTemperatures (sensor) {
// stop receiving live temperature
}
}
You can develop this module outside of the Mobistudy app, in an empty simple Cordova app, so that you can focus on this part of the code only. Once you are happy with it, you can merge this module into Mobsitudy.
Given that testing the interfaces is painful if you need to use a real device, it's
also a good idea to provide a mocked temperature sensor. Let's call it: thermometer.mock.js
.
It will have the same interface as the real thermometer, but will produce some
random data.
To be able to use it, you need to either manually modify the code when the module is imported,
or you can add a flag inside quasar.conf.js
. See that file for examples.
Inside src/router/routes.js Create a new route, or new routes for your task inside the tasks route. For example, if the name of the task is temperature:
{
path: '/tasks',
component: () => import('layouts/TaskLayout.vue'),
children: [
...,
{ path: '/temperature/:studyKey/:taskID', name: 'temperature', component: () => import('pages/tasks/Temperature') },
...
]
}
This implies that you are adding a Vue component, a page, under pages/tasks/ called Temperature.vue The default layout is TaskLayout.vue. If you need another one, you will need to create a new root route.
Add your Vue components. The component should connect to the device using your hardware abstraction (the Bluetooth protocol), collect the data and send the data to the server.
If you need to include images, place your images inside /src/assets, please create a subfolder.
If needed, you can use the db.js module to store some data.
Add your task to TaskListItem.vue inside /src/components/. Define: title, main text and icon.
For icons, use material icons.
this.title = this.$i18n.t('studies.tasks.dataQuery.title')
this.main = this.$i18n.t('studies.tasks.dataQuery.shortDescription')
this.icon = 'insert_chart_outlined'
All text must be added to the /src/i18n/. You are welcome to create a new object, but it has to contain studies.tasks + your own task. For example:
{
studies: {
tasks: {
temperature: {
title: 'Temperature task'
}
}
}
}