Issue #80: Add times an form can be answered

Signed-off-by: Richard Fernando Heise Ferreira's avatarRichard Heise <rfhf19@inf.ufpr.br>
parent e56910ee
Pipeline #23871 passed with stages
in 4 minutes and 45 seconds
......@@ -5,6 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.2.7 - 13/10/2020
## Added
- Property times added to form table #80 (Richard Heise)
## 1.2.6 - 19/06/2020
## Added
- A extra function on optHandler to better handle form edits #77 (Richard Heise)
......
Subproject commit 6cd530d9f009f739a7f285efddae4d29377dad7c
Subproject commit 4471b8cfc730d87b0d87882e4ccc0ffde1da189a
{
"name": "form-creator-api",
"version": "1.2.6",
"version": "1.2.7",
"description": "RESTful API used to manage and answer forms.",
"main": "index.js",
"scripts": {
......
......@@ -22,17 +22,10 @@
import { series, waterfall } from "async";
import * as request from "supertest";
import { expect } from "chai";
import { QueryResult } from "pg";
import * as server from "../../main";
import { EnumHandler, InputType, UpdateType, ValidationType } from "../../utils/enumHandler";
import { TestHandler } from "../../utils/testHandler";
import { OptHandler } from "../../utils/optHandler";
import { Form, FormOptions } from "../../core/form";
import { FormUpdate, FormUpdateOptions } from "../../core/formUpdate";
import { Input, InputOptions, Validation } from "../../core/input";
import { InputUpdate, InputUpdateOptions } from "../../core/inputUpdate";
import { DbHandler } from "../../utils/dbHandler";
import { configs } from "../../utils/config";
import { Fixture } from "../../../test/fixture";
import { formScenario } from "../../../test/scenario";
......@@ -103,6 +96,7 @@ describe("API data controller - form", () => {
expect(res.body).to.be.an("object");
const form: Form = new Form(OptHandler.form(res.body));
expect(form.answerTimes).to.be.equal(false);
TestHandler.testForm(form, new Form(OptHandler.form(formScenario.validForm)));
})
......@@ -300,4 +294,5 @@ describe("API data controller - form", () => {
})
.end(done);
});
});
......@@ -70,7 +70,9 @@ export class FormCtrl {
, title: formResult.title
, description: formResult.description
, inputs: []
, answerTimes: formResult.answerTimes
};
const formUpdate: FormUpdate = DiffHandler.diff(formResult, new Form(formOpts));
callback(null, formUpdate);
......
......@@ -191,6 +191,9 @@ describe("API data controller", () => {
.expect(200)
.expect((res: any) => {
expect(res.body).to.be.an("array");
res.body.sort(function (a: any, b: any) {
return a.id - b.id
})
let j: number = 1;
for (const i of res.body) {
expect(i.id).to.be.eql(j++);
......
......@@ -248,6 +248,7 @@ export class UserCtrl {
, title: form.title
, description: form.description
, answersNumber: 0
, answerTimes: form.answerTimes
, date: ""
}));
......
......@@ -35,31 +35,37 @@ export interface FormOptions {
/** Array of input. containing question */
inputs: InputOptions[];
/** Number of times an user can answer this form */
answerTimes?: boolean;
}
/**
* Form Class to manage project's forms
*/
export class Form {
/** Unique identifier of a Form instance */
public readonly id: number;
/** Form's title. An human-understandable identifier. Not unique */
public readonly title: string;
/** Form Description, as propose */
public readonly description: string;
/** Array of input. containing question */
public readonly inputs: Input[];
/** Unique identifier of a Form instance */
public readonly id: number;
/** Form's title. An human-understandable identifier. Not unique */
public readonly title: string;
/** Form Description, as propose */
public readonly description: string;
/** Array of input. containing question */
public readonly inputs: Input[];
/** Number of times an user can answer this form */
public readonly answerTimes: boolean;
/**
* Creates a new instance of Form Class
* @param options - FormOptions instance to create a form.
*/
constructor(options: FormOptions) {
this.id = options.id ? options.id : null;
this.title = options.title;
this.description = options.description;
this.inputs = options.inputs.map((i: any) => {
return new Input(OptHandler.input(i));
});
}
constructor(options: FormOptions) {
this.id = options.id ? options.id : null;
this.title = options.title;
this.description = options.description;
this.inputs = options.inputs.map((i: any) => {
return new Input(OptHandler.input(i));
});
this.answerTimes = options.answerTimes ? options.answerTimes : false;
}
}
}
......@@ -24,7 +24,7 @@ import { Pool, QueryResult } from "pg";
import { Form } from "../core/form";
import { FormAnswer, FormAnswerOptions } from "../core/formAnswer";
import { InputAnswer, InputAnswerOptions, InputAnswerOptionsDict } from "../core/inputAnswer";
import { ErrorHandler} from "./errorHandler";
import { ErrorHandler } from "./errorHandler";
import { FormQueryBuilder } from "./formQueryBuilder";
import { OptHandler } from "./optHandler";
import { QueryBuilder, QueryOptions } from "./queryBuilder";
......@@ -40,7 +40,7 @@ export class AnswerQueryBuilder extends QueryBuilder {
constructor(builder: FormQueryBuilder, pool: Pool) {
super(pool);
this.formQueryBuilder = builder;
}
}
/**
* Asynchronously write a Answer on database.
......@@ -52,7 +52,7 @@ export class AnswerQueryBuilder extends QueryBuilder {
public write(formAnswer: FormAnswer, cb: (err: Error, result?: FormAnswer) => void) {
waterfall([
(callback: (err: Error, result?: QueryResult) => void) => {
(callback: (err: Error, result?: QueryResult) => void) => {
this.begin((error: Error) => {
callback(error);
});
......@@ -192,12 +192,12 @@ export class AnswerQueryBuilder extends QueryBuilder {
};
this.executeQuery(query, (err: Error, result?: QueryResult) => {
if (err){
if (err) {
cb(err);
return;
}
if (result.rowCount !== 1){
if (result.rowCount !== 1) {
cb(ErrorHandler.notInserted("FormAnswer"));
return;
}
......@@ -264,12 +264,12 @@ export class AnswerQueryBuilder extends QueryBuilder {
};
this.executeQuery(query, (err: Error, result?: QueryResult) => {
if (err){
if (err) {
cb(err);
return;
}
if (result.rowCount !== 1){
if (result.rowCount !== 1) {
cb(ErrorHandler.notInserted("InputsAnswer"));
return;
}
......@@ -278,16 +278,16 @@ export class AnswerQueryBuilder extends QueryBuilder {
});
}
/**
* Asynchronously read a FormAnswer from database.
* @param formAnswerId - FormAnswer identifier to be founded.
* @param cb - Callback function which contains the data read.
* @param cb.err - Error information when the method fails.
* @param cb.formAnswers - FormAnswer object or null if form not exists.
*/
/**
* Asynchronously read a FormAnswer from database.
* @param formAnswerId - FormAnswer identifier to be founded.
* @param cb - Callback function which contains the data read.
* @param cb.err - Error information when the method fails.
* @param cb.formAnswers - FormAnswer object or null if form not exists.
*/
public read(formAnswerId: number, cb: (err: Error, formAnswers?: FormAnswer) => void) {
waterfall([
(callback: (err: Error, result?: QueryResult) => void) => {
(callback: (err: Error, result?: QueryResult) => void) => {
this.begin((error: Error) => {
callback(error);
});
......@@ -333,7 +333,7 @@ export class AnswerQueryBuilder extends QueryBuilder {
*/
public readAll(formId: number, cb: (err: Error, formAnswers?: FormAnswer[]) => void) {
waterfall([
(callback: (err: Error, result?: QueryResult) => void) => {
(callback: (err: Error, result?: QueryResult) => void) => {
this.begin((error: Error) => {
callback(error);
});
......@@ -467,6 +467,7 @@ export class AnswerQueryBuilder extends QueryBuilder {
, title: undefined
, description: undefined
, inputs: []
, answerTimes: answerResult.answerTimes
};
const formAnswerTmp: FormAnswerOptions = {
id: answerResult.id
......@@ -528,7 +529,7 @@ export class AnswerQueryBuilder extends QueryBuilder {
* @param cb.err - Error information when the method fails.
*/
private readSubFormController(inputAnswer: InputAnswerOptions, inputAnswerArray: InputAnswerOptions[], cb: (err: Error) => void) {
if (inputAnswer.subForm === null ) {
if (inputAnswer.subForm === null) {
inputAnswerArray.push(inputAnswer);
cb(null);
return;
......
......@@ -46,7 +46,9 @@ export class DiffHandler {
const formUpdate: FormUpdate = {
form: newForm
, updateDate: new Date()
, changed: ((newForm.title !== oldForm.title) || (newForm.description !== oldForm.description))
, changed: ((newForm.title !== oldForm.title) ||
(newForm.description !== oldForm.description) ||
(newForm.answerTimes !== oldForm.answerTimes))
, inputUpdates: []
};
......@@ -55,7 +57,7 @@ export class DiffHandler {
while ((i < sortedOldInputs.length) && (j < sortedNewInputs.length)) {
if (sortedNewInputs[j]["id"] === sortedOldInputs[i]["id"]) {
if ( sortedNewInputs[j].placement !== sortedOldInputs[i].placement ) {
if (sortedNewInputs[j].placement !== sortedOldInputs[i].placement) {
formUpdate.inputUpdates.push(DiffHandler.swapInput(sortedNewInputs[j], sortedOldInputs[i]));
}
j++;
......
......@@ -95,7 +95,7 @@ export class FormQueryBuilder extends QueryBuilder {
* @param cb.err - Error information when the method fails.
*/
private executeListForms(userId: number, cb: (err: Error, forms?: Form[]) => void) {
const queryString: string = "SELECT t1.id,t1.title,t1.description FROM form t1 \
const queryString: string = "SELECT t1.id,t1.title,t1.description,t1.times FROM form t1 \
INNER JOIN form_owner t2 ON (t1.id=t2.id_form \
AND t2.id_user=$1);";
const query: QueryOptions = {
......@@ -119,10 +119,10 @@ export class FormQueryBuilder extends QueryBuilder {
, title: row["title"]
, description: row["description"]
, inputs: []
, answerTimes: row["times"]
};
const formTmp: Form = new Form(formOpt);
forms.push(formTmp);
}
......@@ -250,6 +250,7 @@ export class FormQueryBuilder extends QueryBuilder {
, title: result.rows[0]["title"]
, description: result.rows[0]["description"]
, inputs: []
, answerTimes: result.rows[0]["times"]
});
callback(null, formTmp);
......@@ -420,6 +421,7 @@ export class FormQueryBuilder extends QueryBuilder {
, title: form.title
, description: form.description
, inputs: inputsTmp
, answerTimes: form.answerTimes
});
anotherCallback(null, formTmp);
}
......@@ -467,7 +469,7 @@ export class FormQueryBuilder extends QueryBuilder {
* @param cb.form - Form or null if form not exists.
*/
private executeReadForm(id: number, cb: (err: Error, form?: QueryResult) => void) {
const queryString: string = "SELECT id, title, description FROM form WHERE id=$1;";
const queryString: string = "SELECT id, title, description, times FROM form WHERE id=$1;";
const query: QueryOptions = {
query: queryString
, parameters: [id]
......@@ -775,14 +777,15 @@ export class FormQueryBuilder extends QueryBuilder {
* @param cb.result - Form identifier or null if any error occurs.
*/
private executeWriteForm(form: Form, cb: (err: Error, result?: number) => void) {
const queryString: string = "INSERT INTO form (title, description) \
VALUES ($1, $2) \
const queryString: string = "INSERT INTO form (title, description, times) \
VALUES ($1, $2, $3) \
RETURNING id;";
const query: QueryOptions = {
query: queryString
, parameters: [
form.title
, form.description
, form.answerTimes
]
};
......@@ -1035,6 +1038,9 @@ export class FormQueryBuilder extends QueryBuilder {
},
(callback: (err: Error) => void) => {
this.executeUpdateForm(form.description, form.id, "description", callback);
},
(callback: (err: Error) => void) => {
this.executeUpdateForm(form.answerTimes, form.id, "times", callback);
}
], (error) => {
cb(error);
......@@ -1194,7 +1200,7 @@ export class FormQueryBuilder extends QueryBuilder {
* @param cb - Callback function.
* @param cb.err - Error information when method fails.
*/
private executeUpdateForm(value: string, id: number, field: string, cb: (err: Error) => void) {
private executeUpdateForm(value: any, id: number, field: string, cb: (err: Error) => void) {
const queryString: string = "UPDATE form SET " + field + " = $1 WHERE id = $2";
const query: QueryOptions = {
query: queryString
......
......@@ -54,7 +54,8 @@ export class OptHandler {
title: obj.title,
description: obj.description,
id: obj.id,
inputs: obj.inputs.map((i: any) => OptHandler.input(i))
inputs: obj.inputs.map((i: any) => OptHandler.input(i)),
answerTimes: obj.answerTimes
};
return option;
......@@ -323,7 +324,8 @@ export class OptHandler {
title: obj.title,
description: obj.description,
id: obj.id,
inputs: obj.inputs.map((i: any) => OptHandler.input(i))
inputs: obj.inputs.map((i: any) => OptHandler.input(i)),
answerTimes: obj.answerTimes
};
return option;
......
......@@ -549,7 +549,8 @@ const form1: Form = {
, inputs: [
Input1
, Input2
]
],
answerTimes: true
};
/** New form with one more Input */
......@@ -562,7 +563,8 @@ const form2: Form = {
, Input2
, Input3
, Input4
]
],
answerTimes: true
};
/** New form with swapped inputs */
const form3: Form = {
......@@ -573,7 +575,8 @@ const form3: Form = {
Input1Otherplacement
, Input2Placement0
, Input3
]
],
answerTimes: true
};
/** New form with inputs that were removed and added */
const form4: Form = {
......@@ -584,7 +587,8 @@ const form4: Form = {
Input1UndefinedID
, Input2UndefinedID
, Input3UndefinedID
]
],
answerTimes: true
};
/** New form resulting from the aplication of all operations */
const form5: Form = {
......@@ -596,7 +600,8 @@ const form5: Form = {
, Input2Placement0id2
, Input3OtherID
, Input4
]
],
answerTimes: true
};
/** New form resulting from the restoration of an Input */
const form6: Form = {
......@@ -607,7 +612,8 @@ const form6: Form = {
Input1
, Input2Placement2id2
, Input3Placement1id3
]
],
answerTimes: true
};
/** New form */
const form7: Form = {
......@@ -618,7 +624,8 @@ const form7: Form = {
Input1Empty
, Input2Placement0idNULL
, Input3idNULL
]
],
answerTimes: true
};
/** New form created with a wrong title */
const form8: Form = {
......@@ -628,7 +635,8 @@ const form8: Form = {
, inputs: [
Input1
, Input2
]
],
answerTimes: true
};
/** New form created */
const formToRead: Form = {
......@@ -638,7 +646,8 @@ const formToRead: Form = {
, inputs: [
Input1
, Input2
]
],
answerTimes: true
};
/** Old form that serves as a base for comparison */
const formBase: Form = {
......@@ -649,7 +658,8 @@ const formBase: Form = {
Input1
, Input2
, Input3
]
],
answerTimes: true
};
/** Another old form that serves as a base for comparison */
const formBase2: Form = {
......@@ -660,7 +670,8 @@ const formBase2: Form = {
Input1
, Input2id2
, Input4Placement2id4
]
],
answerTimes: true
};
/** Another old form that serves as a base for comparison */
const formBase3: Form = {
......@@ -670,7 +681,8 @@ const formBase3: Form = {
, inputs: [
Input1
, mixedInput1
]
],
answerTimes: true
};
/** Empty form used as a base for comparison */
const emptyForm: Form = {
......@@ -678,6 +690,7 @@ const emptyForm: Form = {
, title: "Form Title 1"
, description: "Form Description 1"
, inputs: []
, answerTimes: true
};
/** Expected input update to check an form update with a 'remove' type */
const expInput1: InputUpdate = {
......@@ -1087,6 +1100,7 @@ const expFormtoUpdateNullId: Form = {
, title: "Form Title 1"
, description: "Form Description 1"
, inputs: [Input1]
, answerTimes: true
};
/** Expected form update having null id */
const expFormUpdate: FormUpdate = {
......@@ -1175,7 +1189,8 @@ const formMissingTitle: any = {
OptHandler.input(optsInput1)
, OptHandler.input(optsInput2)
, OptHandler.input(optsInput3)
]
],
answerTimes: true
};
/** Form that has no description atribute */
const formMissingDescription: any = {
......@@ -1185,13 +1200,15 @@ const formMissingDescription: any = {
OptHandler.input(optsInput1)
, OptHandler.input(optsInput2)
, OptHandler.input(optsInput3)
]
],
answerTimes: true
};
/** Form that has no input atribute */
const formMissingInputs: any = {
id: 1
, title: "Form Title 1"
, description: "Form Description 1"
, answerTimes: true
};
/** Input that has no placement atribute */
const inputMissingPlacement: any = {
......@@ -1330,7 +1347,8 @@ const fullFormOptions: FormOptions = {
OptHandler.input(optsInput1)
, OptHandler.input(optsInput2)
, OptHandler.input(optsInput3)
]
],
answerTimes: true
};
const formForAnswer: Form = {
id: 1
......@@ -1340,7 +1358,8 @@ const formForAnswer: Form = {
Input1
, Input2
, Input3
]
],
answerTimes: true
};
/** Valid form created used fullFormOptions */
const tmpForm: Form = new Form(OptHandler.form(fullFormOptions));
......@@ -1422,7 +1441,8 @@ const validFormUpdateObj: any = {
updateObj1
, updateObj2
, updateObj3
]
],
answerTimes: true
};
/** FormUpdate with non-array inputUpdates */
const formUpdateNotArrayInputUpdates: any = {
......@@ -1439,7 +1459,8 @@ const formUpdateMissingForm: any = {
updateObj1
, updateObj2
, updateObj3
]
],
answerTimes: true
};
/** FormUpdate missing the inputs to update */
const formUpdateMissinginputUpdate: any = {
......@@ -1759,7 +1780,8 @@ const formWithValidSubForm1: FormOptions = {
, inputs: [
new Input(inputOptWithValidSubForm1)
, new Input(inputOpt1ForForm8)
]
],
answerTimes: true
};
/** A form with a valid SubForm */
const formWithValidSubForm2: FormOptions = {
......@@ -1769,7 +1791,8 @@ const formWithValidSubForm2: FormOptions = {
, inputs: [
new Input(inputOptWithValidSubForm5)
, new Input(inputOpt1ForForm11)
]
],
answerTimes: true
};
/** A form with a invalid SubForm */
const formWithInvalidSubForm1: FormOptions = {
......@@ -1779,7 +1802,8 @@ const formWithInvalidSubForm1: FormOptions = {
, inputs: [
inputOptWithInvalidSubForm1
, inputOpt1ForForm9
]
],
answerTimes: true
};
/** A form with a invalid SubForm */
......@@ -1790,7 +1814,8 @@ const formWithInvalidSubForm2: FormOptions = {
, inputs: [
inputOptWithInvalidSubForm2
, inputOpt1ForForm9
]
],
answerTimes: true
};
/** A updated version of form 8 */
......@@ -1801,7 +1826,8 @@ const updatedFormWithValidSubForm1: FormOptions = {
, inputs: [
inputOptWithValidSubForm2
, inputOpt2ForForm8
]
],
answerTimes: true
};
/** A updated version of form 8 */
const updatedFormWithValidSubForm2: FormOptions = {
......@@ -1811,7 +1837,8 @@ const updatedFormWithValidSubForm2: FormOptions = {
, inputs: [
inputOpt3ForForm8
, inputOptWithValidSubForm3
]
],
answerTimes: true
};
/** A invalid updated version of form 8 */
......@@ -1823,7 +1850,8 @@ const updatedFormWithInvalidSubForm1: FormOptions = {
inputOpt3ForForm8
, inputOptWithValidSubForm3
, inputOptWithInvalidSubForm4
]