Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 58 additions & 3 deletions app/Controllers/Http/ParticipantController.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,9 +512,30 @@ class ParticipantController {
.orderBy('position', 'asc')
.firstOrFail()
} catch (e) {
return response.requestedRangeNotSatisfiable({
message: `No job could be fetched for participant with identifier ${identifier}.`
})
// Check if study has looping enabled
if (study.loop_enabled) {
// Reset all job states for this participant in this study
await study.resetJobStates(ptcp.id)

// Increment loop count for this participation
await ptcp.studies().pivotQuery()
.where('study_id', study.id)
.increment('loop_count', 1)

// Try to fetch job again
job = await ptcp.jobs()
.where('study_id', study.id)
.whereInPivot('status_id', [1, 2])
.withPivot(['status_id'])
.with('variables.dtype')
.orderBy('pivot_status_id', 'desc')
.orderBy('position', 'asc')
.firstOrFail()
} else {
return response.requestedRangeNotSatisfiable({
message: `No job could be fetched for participant with identifier ${identifier}.`
})
}
}

// Change the status of the job from pending to started
Expand Down Expand Up @@ -597,6 +618,40 @@ class ParticipantController {
.first()

if (job === null) {
// Check if study has looping enabled
if (study.loop_enabled) {
// Reset all job states for this participant in this study
await study.resetJobStates(ptcp.id)

// Increment loop count for this participation
await ptcp.studies().pivotQuery()
.where('study_id', study.id)
.increment('loop_count', 1)

// Try to fetch job again
const newJob = await ptcp.jobs()
.where('study_id', study.id)
.whereInPivot('status_id', [1, 2])
.withPivot(['status_id'])
.with('variables.dtype')
.orderBy('pivot_status_id', 'desc')
.orderBy('position', 'asc')
.first()

if (newJob === null) {
return response.notFound({
message: `There are no jobs available for participant with identifier ${identifier}.`
})
}

return {
data: {
study_id: newJob.study_id,
current_job_index: newJob.position
}
}
}

return response.notFound({
message: `There are no jobs available for participant with identifier ${identifier}.`
})
Expand Down
2 changes: 1 addition & 1 deletion app/Controllers/Http/StudyController.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ class StudyController {
})
}

study.merge(request.only(['name', 'description', 'information', 'active']))
study.merge(request.only(['name', 'description', 'information', 'active', 'loop_enabled']))
study.save()

return transform.item(study, 'StudyTransformer')
Expand Down
53 changes: 50 additions & 3 deletions app/Models/Study.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,40 @@ class Study extends Model {
return this.hasMany('App/Models/Session')
}

/**
* Check if study has looping enabled
* @method hasLoopEnabled
* @return {Boolean}
*/
async hasLoopEnabled () {
return this.loop_enabled === true
}

/**
* Reset all job states for participants in this study (for looping)
* @method resetJobStates
* @param {Number} participantId
* @return {Number} Number of reset job states
*/
async resetJobStates (participantId) {
const studyJobs = await this.jobs().ids()
if (studyJobs.length === 0) {
return 0
}

const result = await Database
.table('job_states')
.where('participant_id', participantId)
.whereIn('job_id', studyJobs)
.where('status_id', 3) // Only reset completed jobs
.update({
status_id: 1, // Reset to pending
updated_at: new Date()
})

return result
}

/**
* Checks if the passed user has sufficient privileges to edit this study
*
Expand Down Expand Up @@ -354,8 +388,20 @@ class Study extends Model {
}))
}

// A job result is stored as JSON in the database and consists of 1) the actual submitted data, 2) some info about the participant,
// 3) some info about the job, 4) the variables data, 5) the study ID and name.
// Get loop_count for this participant in this study
const participation = await Database
.table('participations')
.where('study_id', this.id)
.where('participant_id', ptcpId)
.first()
const loopCount = participation?.loop_count || 0

// A job result is stored as JSON in the database and consists of
// 1) the actual submitted data + loop count,
// 2) some info about the participant,
// 3) some info about the job,
// 4) the variables data,
// 5) the study ID and name.
return await this.jobResults().create({
study_id: this.Id,
participant_id: ptcpId,
Expand All @@ -367,7 +413,8 @@ class Study extends Model {
job_id: job.id,
job_position: job.position,
study_id: this.id,
study_name: this.name,
study_name: this.name,
loop_count: loopCount,
...varData,
...data
})
Expand Down
28 changes: 28 additions & 0 deletions database/migrations/1776506812201_add_loop_fields_schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

/** @type {import('@adonisjs/lucid/src/Schema')} */
const Schema = use('Schema')

class AddLoopFieldsSchema extends Schema {
up () {
this.table('studies', (table) => {
table.boolean('loop_enabled').default(false)
})

this.table('participations', (table) => {
table.integer('loop_count').unsigned().notNullable().defaultTo(0)
})
}

down () {
this.table('studies', (table) => {
table.dropColumn('loop_enabled')
})

this.table('participations', (table) => {
table.dropColumn('loop_count')
})
}
}

module.exports = AddLoopFieldsSchema
8 changes: 8 additions & 0 deletions database/seeds/3-StudySeeder.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ class StudySeeder {
}
console.log(` Study ${s + 1}: ${NUM_JOBS} jobs, ${jobVars.length} job_variable rows`)
}

// Backfill loop_count for all existing job_results
await Database.raw(`
UPDATE job_results
SET data = JSON_SET(data, '$.loop_count', 0)
WHERE JSON_EXTRACT(data, '$.loop_count') IS NULL OR JSON_EXTRACT(data, '$.loop_count') = 0
`)
console.log(' Backfilled loop_count=0 for existing job_results')
}
}

Expand Down
25 changes: 23 additions & 2 deletions resources/components/Studies/page/StudyInfo/StudyInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
auto-grow
label="Information"
/>
<v-switch
v-model="loopEnabled"
:label="$t('studies.loop_enabled')"
:hint="$t('studies.loop_enabled_hint')"
persistent-hint
/>
</div>
<div v-else key="view">
<div class="d-flex justify-end">
Expand All @@ -35,6 +41,15 @@
</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="black--text study-info-content" v-html="$md.render(study.information)" />
<v-divider class="my-4" />
<div class="d-flex align-center">
<v-icon :color="study.loop_enabled ? 'primary' : 'grey'" class="mr-2">
{{ study.loop_enabled ? 'mdi-repeat' : 'mdi-repeat-off' }}
</v-icon>
<span :class="study.loop_enabled ? 'black--text' : 'grey--text'">
{{ study.loop_enabled ? $t('studies.loop_enabled') : $t('studies.loop_disabled') }}
</span>
</div>
</div>
</v-fade-transition>
</div>
Expand All @@ -60,22 +75,28 @@ export default {
data () {
return {
editMode: false,
buffer: this.study?.information ?? ''
buffer: this.study?.information ?? '',
loopEnabled: this.study?.loop_enabled ?? false
}
},
watch: {
study (newData, oldData) {
if (newData?.information === oldData?.information) { return }
this.buffer = newData.information
this.loopEnabled = newData?.loop_enabled ?? false
}
},
methods: {
cancel () {
this.buffer = this.study?.information ?? ''
this.loopEnabled = this.study?.loop_enabled ?? false
this.editMode = false
},
save () {
this.$emit('editted', { information: this.buffer })
this.$emit('editted', {
information: this.buffer,
loop_enabled: this.loopEnabled
})
this.editMode = false
}
}
Expand Down
5 changes: 4 additions & 1 deletion resources/lang/en-US/studies.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,8 @@ export default {
can_edit_short: 'Edit',
search: 'Search user'
}
}
},
loop_enabled: 'Loop tasks',
loop_enabled_hint: 'When enabled, participants will restart from the first task after completing all jobs',
loop_disabled: 'Tasks do not loop'
}
1 change: 1 addition & 0 deletions resources/models/Participation.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class Participation extends Model {
priority: this.number(1),
jobs_count: this.number(0),
job_results_count: this.number(0),
loop_count: this.number(0),
created_at: this.attr(null),
updated_at: this.attr(null)
}
Expand Down
1 change: 1 addition & 0 deletions resources/models/Study.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class Study extends Model {
description: this.string(''),
information: this.string(''),
active: this.boolean(true),
loop_enabled: this.boolean(false),
created_at: this.attr(''),
updated_at: this.attr(''),
deleted_at: this.attr(''),
Expand Down
Loading