Skip to content

Commit

Permalink
Single matrix: prevent duplicate responses in rows fix #7651
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewtelnov committed Jan 10, 2024
1 parent 78ed842 commit 1168e65
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 104 deletions.
11 changes: 11 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ export class RequiredInAllRowsError extends SurveyError {
return this.getLocalizationString("requiredInAllRowsError");
}
}
export class EachRowUniqueError extends SurveyError {
constructor(public text: string, errorOwner: ISurveyErrorOwner = null) {
super(text, errorOwner);
}
public getErrorType(): string {
return "eachrowuniqueeerror";
}
protected getDefaultText(): string {
return this.getLocalizationString("eachRowUniqueError");
}
}
export class MinRowCountError extends SurveyError {
constructor(
public minRowCount: number,
Expand Down
1 change: 1 addition & 0 deletions src/localization/english.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export var englishStrings = {
requiredError: "Response required.",
requiredErrorInPanel: "Response required: answer at least one question.",
requiredInAllRowsError: "Response required: answer questions in all rows.",
eachRowUniqueError: "Each row must have a unique value.",
numericError: "The value should be numeric.",
minError: "The value should not be less than {0}",
maxError: "The value should not be greater than {0}",
Expand Down
87 changes: 65 additions & 22 deletions src/question_matrix.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Helpers } from "./helpers";
import { HashTable, Helpers } from "./helpers";
import { ItemValue } from "./itemvalue";
import { QuestionMatrixBaseModel } from "./martixBase";
import { JsonObject, Serializer } from "./jsonobject";
import { Base } from "./base";
import { SurveyError } from "./survey-error";
import { surveyLocalization } from "./surveyStrings";
import { RequiredInAllRowsError } from "./error";
import { RequiredInAllRowsError, EachRowUniqueError } from "./error";
import { QuestionFactory } from "./questionfactory";
import { LocalizableString, ILocalizableOwner } from "./localizablestring";
import { QuestionDropdownModel } from "./question_dropdown";
Expand All @@ -18,6 +18,7 @@ import { IPlainDataOptions } from "./base-interfaces";
export interface IMatrixData {
onMatrixRowChanged(row: MatrixRowModel): void;
getCorrectedRowValue(value: any): any;
hasErrorInRow(row: MatrixRowModel): boolean;
}

export class MatrixRowModel extends Base {
Expand Down Expand Up @@ -51,18 +52,17 @@ export class MatrixRowModel extends Base {
public get locText(): LocalizableString {
return this.item.locText;
}
public get value() {
public get value(): any {
return this.getPropertyValue("value");
}
public set value(newValue: any) {
newValue = this.data.getCorrectedRowValue(newValue);
this.setPropertyValue("value", newValue);
public set value(val: any) {
val = this.data.getCorrectedRowValue(val);
this.setPropertyValue("value", val);
}
public get rowClasses(): string {
const cssClasses = (<any>this.data).cssClasses;
const hasError = !!(<any>this.data).getErrorByType("requiredinallrowserror");
return new CssClassBuilder().append(cssClasses.row)
.append(cssClasses.rowError, hasError && this.isValueEmpty(this.value))
.append(cssClasses.rowError, this.data.hasErrorInRow(this))
.toString();
}
}
Expand Down Expand Up @@ -261,6 +261,12 @@ export class QuestionMatrixModel
public set isAllRowRequired(val: boolean) {
this.setPropertyValue("isAllRowRequired", val);
}
public get eachRowUnique(): boolean {
return this.getPropertyValue("eachRowUnique");
}
public set eachRowUnique(val: boolean) {
this.setPropertyValue("eachRowUnique", val);
}
public get hasRows(): boolean {
return this.rows.length > 0;
}
Expand Down Expand Up @@ -423,30 +429,63 @@ export class QuestionMatrixModel
supportGoNextPageAutomatic(): boolean {
return this.isMouseDown === true && this.hasValuesInAllRows();
}
protected onCheckForErrors(
errors: Array<SurveyError>,
isOnValueChanged: boolean
) {
private errorsInRow: HashTable<boolean>;
protected onCheckForErrors(errors: Array<SurveyError>, isOnValueChanged: boolean): void {
super.onCheckForErrors(errors, isOnValueChanged);
if (
(!isOnValueChanged || this.hasCssError()) &&
this.hasErrorInRows()
) {
errors.push(new RequiredInAllRowsError(null, this));
this.errorsInRow = undefined;
if (!isOnValueChanged || this.hasCssError()) {
if(this.hasErrorAllRowsRequired()) {
errors.push(new RequiredInAllRowsError(null, this));
}
if(this.hasErrorEachRowUnique()) {
errors.push(new EachRowUniqueError(null, this));
}
}
}
private hasErrorInRows(): boolean {
if (!this.isAllRowRequired) return false;
return !this.hasValuesInAllRows();
private hasErrorAllRowsRequired(): boolean {
return this.isAllRowRequired && !this.hasValuesInAllRows();
}
private hasErrorEachRowUnique(): boolean {
return this.eachRowUnique && this.hasNonUniqueValueInRow();
}
private hasValuesInAllRows(): boolean {
var rows = this.generatedVisibleRows;
if (!rows) rows = this.visibleRows;
if (!rows) return true;
let res = true;
for (var i = 0; i < rows.length; i++) {
if (this.isValueEmpty(rows[i].value)) return false;
const row = rows[i];
const hasValue = !this.isValueEmpty(row.value);
if(!hasValue) {
this.addErrorIntoRow(row);
}
res = res && hasValue;
}
return true;
return res;
}
private hasNonUniqueValueInRow(): boolean {
var rows = this.generatedVisibleRows;
if (!rows) rows = this.visibleRows;
if (!rows) return false;
const hash: HashTable<any> = {};
let res = true;
for (var i = 0; i < rows.length; i++) {
const val = rows[i].value;
const isEmpty = this.isValueEmpty(val);
const isUnique = isEmpty || hash[val] !== true;
if(!isUnique) {
this.addErrorIntoRow(rows[i]);
}
res = res && isUnique;
if(!isEmpty) {
hash[val] = true;
}
}
return !res;
}
private addErrorIntoRow(row: MatrixRowModel): void {
if(!this.errorsInRow) this.errorsInRow = {};
this.errorsInRow[row.name] = true;
}
protected getIsAnswered(): boolean {
return super.getIsAnswered() && this.hasValuesInAllRows();
Expand Down Expand Up @@ -597,6 +636,9 @@ export class QuestionMatrixModel
}
return value;
}
hasErrorInRow(row: MatrixRowModel): boolean {
return !!this.errorsInRow && !!this.errorsInRow[row.name];
}
protected getSearchableItemValueKeys(keys: Array<string>) {
keys.push("columns");
keys.push("rows");
Expand Down Expand Up @@ -654,6 +696,7 @@ Serializer.addClass(
choices: ["initial", "random"],
},
"isAllRowRequired:boolean",
{ name: "eachRowUnique:boolean", category: "validation" },
"hideIfRowsEmpty:boolean",
{ name: "cellComponent", visible: false, default: "survey-matrix-cell" }
],
Expand Down
117 changes: 117 additions & 0 deletions tests/question_matrix_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,123 @@ import { SurveyModel } from "../src/survey";

export default QUnit.module("Survey_QuestionMatrix");

QUnit.test("Matrix Question isAllRowRequired property", function (assert) {
var matrix = new QuestionMatrixModel("q1");
matrix.rows = ["row1", "row2"];
matrix.columns = ["col1", "col2"];
assert.equal(matrix.hasErrors(), false, "There is no errors by default");
matrix.isAllRowRequired = true;
assert.equal(matrix.hasErrors(), true, "There is no errors by default");
});
QUnit.test(
"Matrix Question isAllRowRequired property, value is zero, Bug#2332",
function (assert) {
var matrix = new QuestionMatrixModel("q1");
matrix.fromJSON({
type: "matrix",
name: "question",
isRequired: true,
columns: [
{
value: 0,
text: "No",
},
{
value: 1,
text: "Maybe",
},
{
value: 2,
text: "Yes",
},
],
rows: ["item1", "item2"],
isAllRowRequired: true,
});
var rows = matrix.visibleRows;
assert.equal(matrix.hasErrors(), true, "is Required error");
rows[0].value = 0;
assert.equal(matrix.hasErrors(), true, "isAllRowRequired error");
rows[1].value = 0;
assert.deepEqual(
matrix.value,
{ item1: 0, item2: 0 },
"value set correctly"
);
assert.equal(rows[0].value, 0, "First row value set correctly");
assert.equal(rows[1].value, 0, "Second row value set correctly");
assert.equal(matrix.hasErrors(), false, "There is no errors");
}
);
QUnit.test("Matrix Question eachRowUnique property", function (assert) {
var matrix = new QuestionMatrixModel("q1");
matrix.rows = ["row1", "row2"];
matrix.columns = ["col1", "col2"];
assert.equal(matrix.validate(), true, "validate #1");
matrix.eachRowUnique = true;
assert.equal(matrix.validate(), true, "validate #2");
matrix.value = { row1: "col1" };
assert.equal(matrix.validate(), true, "validate #3");
matrix.value = { row2: "col1" };
assert.equal(matrix.validate(), true, "validate #4");
matrix.value = { row1: "col1", row2: "col1" };
assert.equal(matrix.validate(), false, "validate #5");
matrix.value = { row1: "col1", row2: "col2" };
assert.equal(matrix.validate(), true, "validate #6");
matrix.value = { row1: "col2", row2: "col2" };
assert.equal(matrix.validate(), false, "validate #7");
});
QUnit.test("matirix row, rowClasses property, isAllRowRequired", function (assert) {
var survey = new SurveyModel({
elements: [
{
type: "matrix",
name: "q1",
columns: ["col1", "col2"],
rows: ["row1", "row2"],
isAllRowRequired: true,
},
],
});
survey.css = { matrix: { row: "row", rowError: "row_error" } };
var question = <QuestionMatrixModel>survey.getQuestionByName("q1");
assert.ok(question.cssClasses.row, "Row class is not empty");
assert.equal(question.visibleRows[0].rowClasses, "row", "Set row class");
question.validate();
assert.equal(question.visibleRows[0].rowClasses, "row row_error", "Error for the first row");
question.visibleRows[0].value = "col1";
assert.equal(question.visibleRows[0].rowClasses, "row", "first row value is set");
assert.equal(question.visibleRows[1].rowClasses, "row row_error", "Error for the second row");
});
QUnit.test("matirix row, rowClasses property, eachRowUnique", function (assert) {
const survey = new SurveyModel({
elements: [
{
type: "matrix",
name: "q1",
columns: ["col1", "col2"],
rows: ["row1", "row2"],
eachRowUnique: true,
},
],
});
survey.css = { matrix: { row: "row", rowError: "row_error" } };
const question = <QuestionMatrixModel>survey.getQuestionByName("q1");
assert.ok(question.cssClasses.row, "Row class is not empty");
assert.equal(question.hasErrorInRow(question.visibleRows[0]), false, "hasErrorInRow(0)");
assert.equal(question.visibleRows[0].rowClasses, "row", "Set row class");
question.value = { row1: "col1", row2: "col1" };
question.validate();
assert.equal(question.hasErrorInRow(question.visibleRows[0]), false, "hasErrorInRow(0) #1");
assert.equal(question.visibleRows[0].rowClasses, "row", "first row #1");
assert.equal(question.hasErrorInRow(question.visibleRows[1]), true, "hasErrorInRow(1) #1");
assert.equal(question.visibleRows[1].rowClasses, "row row_error", "second row #1");
question.visibleRows[1].value = "col2";
assert.equal(question.hasErrorInRow(question.visibleRows[0]), false, "hasErrorInRow(0) #1");
assert.equal(question.visibleRows[0].rowClasses, "row", "first row #2");
assert.equal(question.hasErrorInRow(question.visibleRows[1]), false, "hasErrorInRow(1) #2");
assert.equal(question.visibleRows[1].rowClasses, "row", "second row #2");
});
QUnit.test("check row randomization in design mode", (assert) => {

var json = {
Expand Down
Loading

0 comments on commit 1168e65

Please sign in to comment.