Skip to content

Commit fd32ba2

Browse files
authored
Merge pull request #202 from openscript-ch/33-develop-questionnaire-completion-tracking-within-series
33 develop questionnaire completion tracking within series
2 parents f85465f + 53d5384 commit fd32ba2

File tree

17 files changed

+311
-134
lines changed

17 files changed

+311
-134
lines changed

.changeset/olive-hats-turn.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/backend": patch
4+
"@quassel/ui": patch
5+
---
6+
7+
Improve questionnaire completion handling

apps/backend/db/migrations/.snapshot-postgres.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@
283283
"unsigned": false,
284284
"autoincrement": false,
285285
"primary": false,
286-
"nullable": true,
286+
"nullable": false,
287287
"length": 6,
288288
"mappedType": "datetime"
289289
},
@@ -293,7 +293,7 @@
293293
"unsigned": false,
294294
"autoincrement": false,
295295
"primary": false,
296-
"nullable": true,
296+
"nullable": false,
297297
"length": 6,
298298
"mappedType": "datetime"
299299
},
@@ -303,7 +303,7 @@
303303
"unsigned": false,
304304
"autoincrement": false,
305305
"primary": false,
306-
"nullable": true,
306+
"nullable": false,
307307
"length": 255,
308308
"mappedType": "string"
309309
},
@@ -316,13 +316,23 @@
316316
"nullable": true,
317317
"mappedType": "text"
318318
},
319+
"completed_at": {
320+
"name": "completed_at",
321+
"type": "timestamptz",
322+
"unsigned": false,
323+
"autoincrement": false,
324+
"primary": false,
325+
"nullable": true,
326+
"length": 6,
327+
"mappedType": "datetime"
328+
},
319329
"study_id": {
320330
"name": "study_id",
321331
"type": "int",
322332
"unsigned": false,
323333
"autoincrement": false,
324334
"primary": false,
325-
"nullable": true,
335+
"nullable": false,
326336
"mappedType": "integer"
327337
},
328338
"participant_id": {
@@ -331,7 +341,7 @@
331341
"unsigned": false,
332342
"autoincrement": false,
333343
"primary": false,
334-
"nullable": true,
344+
"nullable": false,
335345
"mappedType": "bigint"
336346
}
337347
},
@@ -361,7 +371,6 @@
361371
"id"
362372
],
363373
"referencedTableName": "public.study",
364-
"deleteRule": "set null",
365374
"updateRule": "cascade"
366375
},
367376
"questionnaire_participant_id_foreign": {
@@ -374,7 +383,6 @@
374383
"id"
375384
],
376385
"referencedTableName": "public.participant",
377-
"deleteRule": "set null",
378386
"updateRule": "cascade"
379387
}
380388
},
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Migration } from "@mikro-orm/migrations";
2+
3+
export class Migration20241217133331 extends Migration {
4+
override async up(): Promise<void> {
5+
this.addSql(`alter table "questionnaire" drop constraint "questionnaire_study_id_foreign";`);
6+
this.addSql(`alter table "questionnaire" drop constraint "questionnaire_participant_id_foreign";`);
7+
8+
this.addSql(`alter table "questionnaire" add column "completed_at" timestamptz null;`);
9+
this.addSql(`alter table "questionnaire" alter column "started_at" type timestamptz using ("started_at"::timestamptz);`);
10+
this.addSql(`alter table "questionnaire" alter column "started_at" set not null;`);
11+
this.addSql(`alter table "questionnaire" alter column "ended_at" type timestamptz using ("ended_at"::timestamptz);`);
12+
this.addSql(`alter table "questionnaire" alter column "ended_at" set not null;`);
13+
this.addSql(`alter table "questionnaire" alter column "title" type varchar(255) using ("title"::varchar(255));`);
14+
this.addSql(`alter table "questionnaire" alter column "title" set not null;`);
15+
this.addSql(`alter table "questionnaire" alter column "study_id" type int using ("study_id"::int);`);
16+
this.addSql(`alter table "questionnaire" alter column "study_id" set not null;`);
17+
this.addSql(`alter table "questionnaire" alter column "participant_id" type bigint using ("participant_id"::bigint);`);
18+
this.addSql(`alter table "questionnaire" alter column "participant_id" set not null;`);
19+
this.addSql(
20+
`alter table "questionnaire" add constraint "questionnaire_study_id_foreign" foreign key ("study_id") references "study" ("id") on update cascade;`
21+
);
22+
this.addSql(
23+
`alter table "questionnaire" add constraint "questionnaire_participant_id_foreign" foreign key ("participant_id") references "participant" ("id") on update cascade;`
24+
);
25+
}
26+
27+
override async down(): Promise<void> {
28+
this.addSql(`alter table "questionnaire" drop constraint "questionnaire_study_id_foreign";`);
29+
this.addSql(`alter table "questionnaire" drop constraint "questionnaire_participant_id_foreign";`);
30+
31+
this.addSql(`alter table "questionnaire" drop column "completed_at";`);
32+
33+
this.addSql(`alter table "questionnaire" alter column "started_at" type timestamptz using ("started_at"::timestamptz);`);
34+
this.addSql(`alter table "questionnaire" alter column "started_at" drop not null;`);
35+
this.addSql(`alter table "questionnaire" alter column "ended_at" type timestamptz using ("ended_at"::timestamptz);`);
36+
this.addSql(`alter table "questionnaire" alter column "ended_at" drop not null;`);
37+
this.addSql(`alter table "questionnaire" alter column "title" type varchar(255) using ("title"::varchar(255));`);
38+
this.addSql(`alter table "questionnaire" alter column "title" drop not null;`);
39+
this.addSql(`alter table "questionnaire" alter column "study_id" type int using ("study_id"::int);`);
40+
this.addSql(`alter table "questionnaire" alter column "study_id" drop not null;`);
41+
this.addSql(`alter table "questionnaire" alter column "participant_id" type bigint using ("participant_id"::bigint);`);
42+
this.addSql(`alter table "questionnaire" alter column "participant_id" drop not null;`);
43+
this.addSql(
44+
`alter table "questionnaire" add constraint "questionnaire_study_id_foreign" foreign key ("study_id") references "study" ("id") on update cascade on delete set null;`
45+
);
46+
this.addSql(
47+
`alter table "questionnaire" add constraint "questionnaire_participant_id_foreign" foreign key ("participant_id") references "participant" ("id") on update cascade on delete set null;`
48+
);
49+
}
50+
}

apps/backend/src/research/questionnaires/questionnaire.dto.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiProperty, OmitType, PartialType } from "@nestjs/swagger";
22
import { Type } from "class-transformer";
3-
import { IsDateString, IsNotEmpty } from "class-validator";
3+
import { IsDateString, IsNotEmpty, IsOptional } from "class-validator";
44
import { ParticipantDto } from "../participants/participant.dto";
55
import { StudyDto } from "../studies/study.dto";
66
import { QuestionnaireEntryDto } from "../entries/entry.dto";
@@ -11,24 +11,29 @@ export class QuestionnaireDto {
1111

1212
@ApiProperty({ example: "2024-11-01T07:00:00.000Z", description: "The starting date of the questionnaire" })
1313
@IsDateString()
14-
startedAt?: Date;
14+
startedAt: Date;
1515

1616
@ApiProperty({ example: "2024-11-01T08:00:00.00Z", description: "The ending date of the questionnaire" })
1717
@IsDateString()
18-
endedAt?: Date;
18+
endedAt: Date;
1919

2020
@ApiProperty({ example: "First few months", description: "The title of the questionnaire" })
2121
@IsNotEmpty()
22-
title?: string;
22+
title: string;
2323

2424
@ApiProperty({ example: "We went on holidays for 2 weeks and only spoke Esperanto", description: "The remark of the questionnaire" })
2525
remark?: string;
2626

27+
@ApiProperty({ example: "2024-11-01T07:00:00.000Z", description: "The date the questionnaire was completed" })
28+
@IsOptional()
29+
@IsDateString()
30+
completedAt?: Date;
31+
2732
@Type(() => StudyDto)
28-
study?: StudyDto;
33+
study: StudyDto;
2934

3035
@Type(() => ParticipantDto)
31-
participant?: ParticipantDto;
36+
participant: ParticipantDto;
3237

3338
@Type(() => Array<QuestionnaireEntryDto>)
3439
entries?: QuestionnaireEntryDto[];

apps/backend/src/research/questionnaires/questionnaire.entity.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@ import { Entry } from "../entries/entry.entity";
77
@Entity()
88
export class Questionnaire extends BaseEntity {
99
@Property()
10-
startedAt?: Date;
10+
startedAt: Date;
1111

1212
@Property()
13-
endedAt?: Date;
13+
endedAt: Date;
1414

1515
@Property()
16-
title?: string;
16+
title: string;
1717

1818
@Property({ columnType: "text" })
1919
remark?: string;
2020

21+
@Property()
22+
completedAt?: Date;
23+
2124
@ManyToOne()
22-
study?: Study;
25+
study: Study;
2326

2427
@ManyToOne()
25-
participant?: Participant;
28+
participant: Participant;
2629

2730
@OneToMany(() => Entry, (entry) => entry.questionnaire)
2831
entries = new Collection<Entry>(this);

apps/backend/src/research/questionnaires/questionnaires.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EntityRepository, EntityManager, UniqueConstraintViolationException, FilterQuery } from "@mikro-orm/core";
22
import { InjectRepository } from "@mikro-orm/nestjs";
3-
import { Injectable, UnprocessableEntityException } from "@nestjs/common";
3+
import { BadRequestException, Injectable, UnprocessableEntityException } from "@nestjs/common";
44
import { QuestionnaireCreationDto, QuestionnaireMutationDto } from "./questionnaire.dto";
55
import { Questionnaire } from "./questionnaire.entity";
66
import { Entry } from "../entries/entry.entity";
@@ -20,6 +20,10 @@ export class QuestionnairesService {
2020
questionnaire.assign(questionnaireCreationDto, { em: this.em });
2121

2222
const prevQuestionnaire = await this.findLatestByParticipant(questionnaire.participant!.id);
23+
24+
if (prevQuestionnaire && !prevQuestionnaire.completedAt)
25+
throw new BadRequestException("Complete the previous questionniare before starting a new one.");
26+
2327
this.validateStartDate(questionnaire, prevQuestionnaire);
2428

2529
await prevQuestionnaire?.populate(["entries.entryLanguages"]);

0 commit comments

Comments
 (0)