Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quill validation on frontend components #699

Merged
merged 13 commits into from
Sep 21, 2022
9 changes: 5 additions & 4 deletions frontend/riskofbias/robScoreCleanup/containers/MetricForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {inject, observer} from "mobx-react";

import h from "shared/utils/helpers";

import {ScoreInput, ScoreNotesInput} from "../../robStudyForm/ScoreForm";
import QuillTextInput from "shared/components/QuillTextInput";
import {ScoreInput} from "../../robStudyForm/ScoreForm";

@inject("store")
@observer
Expand Down Expand Up @@ -37,10 +38,10 @@ class MetricForm extends React.Component {
</p>
</div>
<div className="col-md-7">
<ScoreNotesInput
scoreId={-1}
<QuillTextInput
className="score-editor"
value={store.formNotes}
handleChange={value => store.setFormNotes(value)}
onChange={value => store.setFormNotes(value)}
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/riskofbias/robStudyForm/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class Root extends Component {

return (
<div className="riskofbias-display">
<ScrollToErrorBox error={store.error} />
<form>
{store.domainIds.map(domainId => {
return <Domain key={domainId} domainId={domainId} />;
Expand All @@ -37,6 +36,7 @@ class Root extends Component {
</p>
</div>
) : null}
<ScrollToErrorBox error={store.error} />
<div className="d-flex justify-content-between">
<button
className="btn btn-primary"
Expand Down
206 changes: 98 additions & 108 deletions frontend/riskofbias/robStudyForm/ScoreForm.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, {Component} from "react";
import PropTypes from "prop-types";
import ReactQuill from "react-quill";
import {observer, inject} from "mobx-react";

import ScoreIcon from "riskofbias/robTable/components/ScoreIcon";
import QuillTextInput from "shared/components/QuillTextInput";
import SelectInput from "shared/components/SelectInput";
import Spacer from "shared/components/Spacer";
import TextInput from "shared/components/TextInput";
Expand All @@ -27,7 +27,7 @@ class ScoreInput extends Component {
}
}
render() {
const {choices, value, handleChange} = this.props;
const {choices, value, handleChange, errors} = this.props;
return (
<>
<SelectInput
Expand All @@ -37,6 +37,7 @@ class ScoreInput extends Component {
multiple={false}
value={value}
handleSelect={handleChange}
errors={errors}
/>
<ScoreIcon score={value} />
</>
Expand All @@ -48,25 +49,7 @@ ScoreInput.propTypes = {
value: PropTypes.number.isRequired,
handleChange: PropTypes.func.isRequired,
defaultValue: PropTypes.number.isRequired,
};

class ScoreNotesInput extends Component {
render() {
const {scoreId, value, handleChange} = this.props;
return (
<ReactQuill
id={`${scoreId}-notes`}
value={value}
onChange={handleChange}
className="score-editor"
/>
);
}
}
ScoreNotesInput.propTypes = {
scoreId: PropTypes.number.isRequired,
value: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
errors: PropTypes.array,
};

@inject("store")
Expand All @@ -86,103 +69,110 @@ class ScoreForm extends Component {
}
),
showOverrideCreate = score.is_default === true,
isOverride = score.is_default === false;
isOverride = score.is_default === false,
errorClass = Object.keys(score.errors).length > 0 ? "border border-danger " : "";

return (
<div className="score-form container-fluid ">
<>
{isOverride ? <Spacer borderStyle="4px dashed #323a45" /> : null}
<div className="row">
<div className="col-md-3">
{editableMetricHasOverrides ? (
<TextInput
id={`${score.id}-label`}
label="Label"
name={`label-id-${score.id}`}
onChange={e => {
store.updateScoreState(score, "label", e.target.value);
}}
value={score.label}
/>
) : null}
</div>
<div className="col-md-9">
{showOverrideCreate ? (
<button
className="btn btn-primary float-right"
type="button"
onClick={() => {
store.createScoreOverride({
metric: score.metric_id,
riskofbias: score.riskofbias_id,
});
}}>
<i className="fa fa-plus"></i>&nbsp;Create new override
</button>
) : null}
<div className={`score-form container-fluid ${errorClass}`}>
<div className="row mt-3">
<div className="col-md-3">
{editableMetricHasOverrides ? (
<TextInput
id={`${score.id}-label`}
label="Label"
name={`label-id-${score.id}`}
onChange={e => {
store.updateScoreState(score, "label", e.target.value);
}}
value={score.label}
/>
) : null}
</div>
<div className="col-md-9">
{showOverrideCreate ? (
<button
className="btn btn-primary float-right"
type="button"
onClick={() => {
store.createScoreOverride({
metric: score.metric_id,
riskofbias: score.riskofbias_id,
});
}}>
<i className="fa fa-plus"></i>&nbsp;Create new override
</button>
) : null}

{isOverride ? (
<button
className="btn btn-danger float-right"
type="button"
onClick={() => store.deleteScoreOverride(score.id)}>
<i className="fa fa-trash"></i>&nbsp;Delete override
</button>
) : null}
{isOverride ? (
<button
className="btn btn-danger float-right"
type="button"
onClick={() => store.deleteScoreOverride(score.id)}>
<i className="fa fa-trash"></i>&nbsp;Delete override
</button>
) : null}

{editableMetricHasOverrides ? (
score.is_default ? (
<b title="Unless otherwise specified, all content will use this value">
<i className="fa fa-check-square-o" />
&nbsp;Default judgment
</b>
) : (
<b title="Only selected override content will use this value">
<i className="fa fa-square-o" />
&nbsp;Override judgment
</b>
)
) : null}
{editableMetricHasOverrides ? (
score.is_default ? (
<b title="Unless otherwise specified, all content will use this value">
<i className="fa fa-check-square-o" />
&nbsp;Default judgment
</b>
) : (
<b title="Only selected override content will use this value">
<i className="fa fa-square-o" />
&nbsp;Override judgment
</b>
)
) : null}
</div>
</div>
</div>
<div className="row">
{showScoreInput ? (
<div className="col-md-3">
<ScoreInput
choices={scoreChoices}
value={score.score}
defaultValue={defaultScoreChoice}
handleChange={value => {
store.updateScoreState(score, "score", parseInt(value));
}}
/>
<SelectInput
id={`${score.id}-direction`}
label="Bias direction"
choices={direction_choices}
multiple={false}
value={score.bias_direction}
handleSelect={value => {
store.updateScoreState(
score,
"bias_direction",
parseInt(value)
);
<div className="row">
{showScoreInput ? (
<div className="col-md-3">
<ScoreInput
choices={scoreChoices}
value={score.score}
defaultValue={defaultScoreChoice}
handleChange={value => {
store.updateScoreState(score, "score", parseInt(value));
}}
errors={score.errors.score}
/>
<SelectInput
id={`${score.id}-direction`}
label="Bias direction"
choices={direction_choices}
multiple={false}
value={score.bias_direction}
handleSelect={value => {
store.updateScoreState(
score,
"bias_direction",
parseInt(value)
);
}}
errors={score.errors.bias_direction}
/>
</div>
) : null}
<div className="col-md-9">
<QuillTextInput
id={`${score.id}-notes`}
className="score-editor"
value={score.notes}
onChange={htmlContent => {
store.updateScoreState(score, "notes", htmlContent);
}}
errors={score.errors.notes}
/>
</div>
) : null}
<div className="col-md-9">
<ScoreNotesInput
scoreId={score.id}
value={score.notes}
handleChange={htmlContent => {
store.updateScoreState(score, "notes", htmlContent);
}}
/>
</div>
{score.is_default ? null : <ScoreOverrideForm score={score} />}
</div>
{score.is_default ? null : <ScoreOverrideForm score={score} />}
</div>
</>
);
}
}
Expand All @@ -192,4 +182,4 @@ ScoreForm.propTypes = {
store: PropTypes.object,
};

export {ScoreForm, ScoreInput, ScoreNotesInput};
export {ScoreForm, ScoreInput};
14 changes: 12 additions & 2 deletions frontend/riskofbias/robStudyForm/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class RobFormStore extends StudyRobStore {
score_symbol: this.settings.score_metadata.symbols[score.score],
score_shade: this.settings.score_metadata.colors[score.score],
score_description: this.settings.score_metadata.choices[score.score],
errors: {},
});
}

Expand Down Expand Up @@ -173,6 +174,7 @@ class RobFormStore extends StudyRobStore {
)}`;

this.error = null;
this.editableScores.forEach(score => (score.errors = {}));
return fetch(url, opts)
.then(response => {
if (response.ok) {
Expand All @@ -182,8 +184,16 @@ class RobFormStore extends StudyRobStore {
this.setChangedSavedDiv();
}
} else {
response.text().then(text => {
this.error = text;
response.json().then(data => {
if (data.scores) {
this.editableScores.forEach(
(score, index) => (score.errors = data.scores[index])
);
this.error =
"Changes could not be saved. Review the form above for error messages.";
} else {
this.error = data;
}
});
}
})
Expand Down
6 changes: 4 additions & 2 deletions frontend/shared/components/QuillTextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import HelpText from "./HelpText";

class QuillTextInput extends Component {
renderField(fieldId) {
const {errors} = this.props;
const {errors, className} = this.props,
extraClasses = className ? `${this.props.className} ` : "";

return (
<ReactQuill
id={fieldId}
className={inputClass("col-12 p-0", errors)}
className={inputClass(`${extraClasses}col-12 p-0`, errors)}
type="text"
required={this.props.required}
value={this.props.value}
Expand Down Expand Up @@ -45,6 +46,7 @@ QuillTextInput.propTypes = {
onChange: PropTypes.func.isRequired,
required: PropTypes.bool,
value: PropTypes.string.isRequired,
className: PropTypes.string,
};

export default QuillTextInput;
1 change: 1 addition & 0 deletions frontend/shared/utils/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ const helpers = {
// scroll into view if not currently visible; can optionally specify an offset too.
// eslint-disable-next-line react/no-find-dom-node
const node = ReactDOM.findDOMNode(reactNode);
options = options || {};
if (node) {
setTimeout(() => {
const rect = node.getBoundingClientRect(),
Expand Down
10 changes: 9 additions & 1 deletion hawc/apps/common/api/mixins.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import DataError
from django.shortcuts import get_object_or_404
from django.utils.encoding import force_str
from rest_framework import status
from rest_framework.exceptions import ValidationError as DrfValidationError
from rest_framework.response import Response


Expand Down Expand Up @@ -67,7 +69,13 @@ def get_update_bulk_dict(self, serializer, data):
update_bulk_dict = {}
for field_name, field in serializer.fields.items():
if field_name in data and not field.read_only:
update_bulk_dict[field.source or field_name] = data[field_name]
value = data[field_name]
if validator := getattr(serializer, f"validate_{field_name}", None):
try:
value = validator(value)
except ValidationError as err:
raise DrfValidationError(err)
update_bulk_dict[field.source or field_name] = value
return update_bulk_dict

def pre_save_bulk(self, queryset, update_bulk_dict):
Expand Down
Loading