From 5b791775b6cdcba5b155c88cf4c8ea754a1604a6 Mon Sep 17 00:00:00 2001
From: Hanny Prastya Hariyadi
Date: Mon, 2 Oct 2023 16:18:59 +0900
Subject: [PATCH 01/23] Remove locale and lookupFDashLocalizations in view
layer
---
lib/extensions/function_extension.dart | 26 ++++++
lib/fhir_types/src/date_time_picker.dart | 12 +--
.../model/aggregation/src/aggregator.dart | 8 +-
.../aggregation/src/narrative_aggregator.dart | 12 ++-
.../questionnaire_response_aggregator.dart | 9 +-
.../src/total_score_aggregator.dart | 10 ++-
.../model/item/answer/src/answer_model.dart | 54 ++++++------
.../answer/src/attachment_answer_model.dart | 29 ++++---
.../item/answer/src/boolean_answer_model.dart | 4 +-
.../item/answer/src/coding_answer_model.dart | 38 ++++----
.../src/coding_answer_option_model.dart | 3 +-
.../answer/src/datetime_answer_model.dart | 27 +++---
.../answer/src/numerical_answer_model.dart | 47 +++++-----
.../item/answer/src/string_answer_model.dart | 79 +++++++++--------
.../answer/src/unsupported_answer_model.dart | 4 +-
.../model/item/src/question_item_model.dart | 36 +++-----
.../model/item/src/response_item_model.dart | 69 ++++++++-------
.../src/questionnaire_response_model.dart | 33 +++----
.../model/src/rendering_string.dart | 4 +
.../answer/src/attachment_answer_filler.dart | 6 +-
.../answer/src/boolean_answer_filler.dart | 14 +--
.../item/answer/src/coding_answer_filler.dart | 32 ++++---
.../answer/src/datetime_answer_filler.dart | 3 +-
.../answer/src/numerical_answer_filler.dart | 37 +++++---
.../item/answer/src/string_answer_filler.dart | 17 +++-
.../view/item/src/group_item.dart | 5 +-
.../src/question_response_item_filler.dart | 8 +-
.../item/src/questionnaire_item_filler.dart | 11 ++-
.../src/questionnaire_item_filler_title.dart | 10 ++-
.../src/questionnaire_complete_button.dart | 5 +-
.../view/src/questionnaire_filler.dart | 60 ++++++-------
.../view/src/questionnaire_page_scaffold.dart | 3 +-
.../view/src/questionnaire_scroller.dart | 87 +++++++++----------
.../view/src/questionnaire_scroller_page.dart | 3 -
.../view/src/questionnaire_stepper.dart | 6 +-
.../view/src/questionnaire_stepper_page.dart | 2 -
.../src/questionnaire_stepper_page_view.dart | 21 +++--
.../view/src/questionnaire_theme.dart | 34 ++++----
lib/questionnaires/view/src/xhtml.dart | 11 ++-
39 files changed, 494 insertions(+), 385 deletions(-)
create mode 100644 lib/extensions/function_extension.dart
diff --git a/lib/extensions/function_extension.dart b/lib/extensions/function_extension.dart
new file mode 100644
index 00000000..656c0428
--- /dev/null
+++ b/lib/extensions/function_extension.dart
@@ -0,0 +1,26 @@
+/// An extension on [Function] that provides a method to call functions
+/// safely, which means if the function throws an error, it will catch that
+/// error and return `null`, instead of letting the error propagate.
+///
+/// This can be useful when working with functions that may throw exceptions
+/// but you don't want to handle those exceptions explicitly or when you
+/// prefer to deal with a `null` return value in the case of an error.
+extension TryOrNull on Function {
+ /// Calls the function safely and returns the result of the function if it's
+ /// successful, or `null` if an error occurs.
+ ///
+ /// Here, `T` represents the expected return type of the function.
+ ///
+ /// Example:
+ /// ```dart
+ /// Function riskyFunction = () => throw Exception('Oops!');
+ /// final result = riskyFunction.callSafely(); // result will be `null`.
+ /// ```
+ T? callSafely() {
+ try {
+ return this.call() as T;
+ } catch (e) {
+ return null;
+ }
+ }
+}
diff --git a/lib/fhir_types/src/date_time_picker.dart b/lib/fhir_types/src/date_time_picker.dart
index f1bf5769..23b0ad0e 100644
--- a/lib/fhir_types/src/date_time_picker.dart
+++ b/lib/fhir_types/src/date_time_picker.dart
@@ -76,8 +76,8 @@ class _FhirDateTimePickerState extends State {
if (value == null || dateTime == null) return '';
return (widget.pickerType == Time)
- ? DateFormat.jm(locale.toString()).format(dateTime)
- : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime);
+ ? DateFormat.jm(locale.toString()).format(dateTime)
+ : value.format(locale, withTimeZone: widget.pickerType == FhirDateTime);
}
Future _showPicker(Locale locale) async {
@@ -113,13 +113,15 @@ class _FhirDateTimePickerState extends State {
// Get new BuildContext with overridden locale
builder: (context) {
// Get time of day format of current locale
- final timeOfDayFormat = MaterialLocalizations.of(context).timeOfDayFormat();
+ final timeOfDayFormat =
+ MaterialLocalizations.of(context).timeOfDayFormat();
return MediaQuery(
data: MediaQuery.of(context).copyWith(
// Workaround for time picker validation bug in `input` mode with locales specifying a 24h TimeOfDayFormat.
// - https://github.com/sujrd/faiadashu/pull/32#issuecomment-1678639964
// - https://github.com/flutter/flutter/issues/85527
- alwaysUse24HourFormat: timeOfDay24hFormats.contains(timeOfDayFormat),
+ alwaysUse24HourFormat:
+ timeOfDay24hFormats.contains(timeOfDayFormat),
),
child: child!,
);
@@ -157,7 +159,7 @@ class _FhirDateTimePickerState extends State {
@override
Widget build(BuildContext context) {
- final locale = widget.locale ?? Localizations.localeOf(context);
+ final locale = Localizations.localeOf(context);
// There is no Locale in initState.
if (!_fieldInitialized) {
diff --git a/lib/questionnaires/model/aggregation/src/aggregator.dart b/lib/questionnaires/model/aggregation/src/aggregator.dart
index ac756372..d62cf3c1 100644
--- a/lib/questionnaires/model/aggregation/src/aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/aggregator.dart
@@ -1,3 +1,4 @@
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
import 'package:flutter/material.dart';
@@ -8,11 +9,16 @@ import 'package:flutter/material.dart';
abstract class Aggregator extends ValueNotifier {
late final QuestionnaireResponseModel questionnaireResponseModel;
late final Locale locale;
+ final FDashLocalizations localizations;
final bool autoAggregate;
/// [autoAggregate] specifies whether it should attach listeners to the
/// questionnaire and aggregate when the questionnaire changes.
- Aggregator(T initialValue, {this.autoAggregate = true}) : super(initialValue);
+ Aggregator(
+ T initialValue, {
+ required this.localizations,
+ this.autoAggregate = true,
+ }) : super(initialValue);
// ignore: use_setters_to_change_properties
/// Initialize the aggregator.
diff --git a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
index 821cf9c4..c392cc6a 100644
--- a/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/narrative_aggregator.dart
@@ -18,8 +18,12 @@ class NarrativeAggregator extends Aggregator {
status: NarrativeStatus.empty,
);
- NarrativeAggregator()
- : super(NarrativeAggregator.emptyNarrative, autoAggregate: false);
+ NarrativeAggregator({required FDashLocalizations localizations})
+ : super(
+ NarrativeAggregator.emptyNarrative,
+ localizations: localizations,
+ autoAggregate: false,
+ );
@override
void init(QuestionnaireResponseModel questionnaireResponseModel) {
@@ -121,7 +125,7 @@ class NarrativeAggregator extends Aggregator {
if (invalid) {
div.write(
- '${lookupFDashLocalizations(locale).dataAbsentReasonAsTextOutput} ',
+ '${localizations.dataAbsentReasonAsTextOutput} ',
);
}
@@ -129,7 +133,7 @@ class NarrativeAggregator extends Aggregator {
div.write('***
');
} else if (dataAbsentReason == dataAbsentReasonAskedButDeclinedCode) {
div.write(
- 'X ${lookupFDashLocalizations(locale).dataAbsentReasonAskedDeclinedOutput}
',
+ 'X ${localizations.dataAbsentReasonAskedDeclinedOutput}
',
);
} else {
final filledAnswers = itemModel.answeredAnswerModels;
diff --git a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
index a5af7688..ea0c811c 100644
--- a/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/questionnaire_response_aggregator.dart
@@ -1,5 +1,6 @@
import 'package:faiadashu/coding/coding.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/questionnaires.dart';
import 'package:fhir/r4.dart';
@@ -13,8 +14,12 @@ class QuestionnaireResponseAggregator
extends Aggregator {
static final Logger _logger = Logger(QuestionnaireResponseAggregator);
- QuestionnaireResponseAggregator()
- : super(QuestionnaireResponse(), autoAggregate: false);
+ QuestionnaireResponseAggregator({required FDashLocalizations localizations})
+ : super(
+ QuestionnaireResponse(),
+ localizations: localizations,
+ autoAggregate: false,
+ );
QuestionnaireResponseItem? _fromQuestionItem(
QuestionItemModel itemModel,
diff --git a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
index 313929e2..a1cec24d 100644
--- a/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
+++ b/lib/questionnaires/model/aggregation/src/total_score_aggregator.dart
@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
+import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
import 'package:fhir/r4.dart';
@@ -16,8 +17,13 @@ class TotalScoreAggregator extends Aggregator {
static final _logger = Logger(TotalScoreAggregator);
late final QuestionItemModel? totalScoreItem;
- TotalScoreAggregator({bool autoAggregate = true})
- : super(Decimal(0), autoAggregate: autoAggregate);
+ TotalScoreAggregator(
+ {required FDashLocalizations localizations, bool autoAggregate = true})
+ : super(
+ Decimal(0),
+ localizations: localizations,
+ autoAggregate: autoAggregate,
+ );
@override
void init(QuestionnaireResponseModel questionnaireResponseModel) {
diff --git a/lib/questionnaires/model/item/answer/src/answer_model.dart b/lib/questionnaires/model/item/answer/src/answer_model.dart
index 70312201..8d9198d0 100644
--- a/lib/questionnaires/model/item/answer/src/answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/answer_model.dart
@@ -1,5 +1,5 @@
-import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/faiadashu.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/validation_error.dart';
import 'package:fhir/r4.dart';
import 'package:flutter/material.dart';
@@ -74,16 +74,16 @@ abstract class AnswerModel extends ResponseNode {
/// Validates a new input value. Does not change the [value].
///
- /// Returns null when [inputValue] is valid, or a localized message when it is not.
- ///
/// This is used to validate external input from a view.
- String? validateInput(I? inputValue);
+ ///
+ /// Throws [ValidationError] when when [inputValue] is invalid.
+ void validateInput(I? inputValue);
/// Validates a value against the constraints of the answer model.
/// Does not change the [value] of the answer model.
///
- /// Returns null when it is valid, or a localized message when it is not.
- String? validateValue(V? inputValue);
+ /// Throws [ValidationError] when when [inputValue] is invalid.
+ void validateValue(V? inputValue);
/// Validates whether the current [value] will pass the completeness check.
///
@@ -93,29 +93,25 @@ abstract class AnswerModel extends ResponseNode {
/// Since an individual answer does not know whether it is required, this
/// is not taken into account.
///
- /// Returns null when the answer is valid, or an error text,
- /// when it is not.
- ///
- String? validate({
+ /// Returns an empty list when the answer is valid, otherwise
+ /// Returns a list of [ValidationError].
+ List validate({
bool updateErrorText = true,
bool notifyListeners = false,
}) {
- final newErrorText = validateValue(
- value,
- );
-
- if (errorText == newErrorText) {
- return newErrorText;
+ try {
+ validateValue(value);
+
+ if (notifyListeners) {
+ this.notifyListeners();
+ }
+ return [];
+ } on ValidationError catch (exception) {
+ if (updateErrorText) {
+ _error = exception;
+ }
+ return [exception];
}
-
- if (updateErrorText) {
- errorText = newErrorText;
- }
- if (notifyListeners) {
- this.notifyListeners();
- }
-
- return newErrorText;
}
/// Returns whether any answer (valid or invalid) has been provided.
@@ -124,12 +120,14 @@ abstract class AnswerModel extends ResponseNode {
/// Returns whether this question is unanswered.
bool get isEmpty;
- String? errorText;
+ ValidationError? _error;
/// Returns an error text for display in the answer's control.
///
/// This might return an error text from the parent [QuestionItemModel].
- String? get displayErrorText => errorText ?? responseItemModel.errorText;
+ String? displayErrorText(FDashLocalizations localizations) =>
+ _error?.getMessage(localizations) ??
+ responseItemModel.getErrorText(localizations);
/// Returns a [QuestionnaireResponseAnswer] based on the current value.
///
diff --git a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
index ffeab84e..abdeae1f 100644
--- a/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/attachment_answer_model.dart
@@ -1,8 +1,8 @@
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_size_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/mime_types_error.dart';
import 'package:fhir/r4.dart';
-import 'package:filesize/filesize.dart';
class AttachmentAnswerModel extends AnswerModel {
final num maxSize;
@@ -10,14 +10,18 @@ class AttachmentAnswerModel extends AnswerModel {
AttachmentAnswerModel(super.responseModel)
: maxSize = responseModel.questionnaireItem.extension_
- ?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/maxSize')
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/StructureDefinition/maxSize')
?.valueDecimal
- ?.value ?? 0,
+ ?.value ??
+ 0,
mimeTypes = responseModel.questionnaireItem.extension_
- ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/mimeType')
+ ?.whereExtensionIs(
+ 'http://hl7.org/fhir/StructureDefinition/mimeType')
?.map((ext) => ext.valueCode?.value ?? '')
.where((mimeType) => mimeType != '')
- .toList() ?? [];
+ .toList() ??
+ [];
@override
RenderingString get display => (value != null)
@@ -25,25 +29,26 @@ class AttachmentAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(Attachment? inValue) {
- return validateValue(inValue);
+ void validateInput(Attachment? inValue) {
+ validateValue(inValue);
}
@override
- String? validateValue(Attachment? inputValue) {
+ void validateValue(Attachment? inputValue) {
if (inputValue == null) return null;
if (maxSize > 0) {
final attachmentSize = inputValue.size?.value;
if (attachmentSize == null || attachmentSize > maxSize) {
- return lookupFDashLocalizations(locale).validatorMaxSize(filesize(maxSize));
+ throw MaxSizeError(nodeUid, maxSize);
}
}
if (mimeTypes.isNotEmpty) {
final attachmentMimeType = inputValue.contentType?.value;
- if (attachmentMimeType == null || !mimeTypes.contains(attachmentMimeType)) {
- return lookupFDashLocalizations(locale).validatorMimeTypes(mimeTypes.join(', '));
+ if (attachmentMimeType == null ||
+ !mimeTypes.contains(attachmentMimeType)) {
+ throw MimeTypesError(nodeUid, mimeTypes);
}
}
diff --git a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
index 076a379c..f00eaf54 100644
--- a/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/boolean_answer_model.dart
@@ -23,12 +23,12 @@ class BooleanAnswerModel extends AnswerModel {
);
@override
- String? validateInput(Boolean? inValue) {
+ void validateInput(Boolean? inValue) {
return null;
}
@override
- String? validateValue(Boolean? inputValue) {
+ void validateValue(Boolean? inputValue) {
return null;
}
diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
index b38e86e0..775ab28a 100644
--- a/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/coding_answer_model.dart
@@ -3,6 +3,9 @@ import 'package:faiadashu/fhir_types/fhir_types.dart';
import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_occurs_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_occurs_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/single_selection_or_open_string_error.dart';
import 'package:faiadashu/questionnaires/questionnaires.dart';
import 'package:fhir/r4.dart';
@@ -154,15 +157,6 @@ class CodingAnswerModel extends AnswerModel {
bool get isOptionsOrString => qi.type == QuestionnaireItemType.open_choice;
- RenderingString get openLabel => RenderingString.fromText(
- qi.extension_
- ?.extensionOrNull(
- 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel',
- )
- ?.valueString ??
- lookupFDashLocalizations(locale).fillerOpenCodingOtherLabel,
- );
-
String _nextOptionUid() => _answerOptions.length.toString();
void _addAnswerOptionFromValueSetCoding(Coding coding) {
@@ -231,6 +225,17 @@ class CodingAnswerModel extends AnswerModel {
_createAnswerOptions();
}
+ RenderingString getOpenLabel(FDashLocalizations localizations) {
+ return RenderingString.fromText(
+ _openLabel ?? localizations.fillerOpenCodingOtherLabel,
+ );
+ }
+
+ String? get _openLabel => qi.extension_
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/uv/sdc/StructureDefinition/questionnaire-sdc-openLabel')
+ ?.valueString;
+
Iterable toDisplay({bool includeMedia = true}) {
final value = this.value;
if (value == null) {
@@ -301,14 +306,14 @@ class CodingAnswerModel extends AnswerModel {
}
@override
- String? validateInput(OptionsOrString? inValue) {
+ void validateInput(OptionsOrString? inValue) {
return validateValue(inValue);
}
@override
- String? validateValue(OptionsOrString? inValue) {
+ void validateValue(OptionsOrString? inValue) {
if (inValue == null) {
- return null;
+ return;
}
final selectedOptionsCount = inValue.selectedOptions?.length ?? 0;
@@ -318,21 +323,18 @@ class CodingAnswerModel extends AnswerModel {
if (!(questionnaireItemModel.questionnaireItem.repeats?.value ?? false)) {
if (totalCount != 1) {
- return lookupFDashLocalizations(locale)
- .validatorSingleSelectionOrSingleOpenString(openLabel.plainText);
+ throw SingleSelectionOrOpenStringError(nodeUid, _openLabel);
}
}
if (totalCount < minOccurs) {
- return lookupFDashLocalizations(locale).validatorMinOccurs(minOccurs);
+ throw MinOccursError(nodeUid, minOccurs);
}
final maxOccurs = this.maxOccurs;
if (maxOccurs != null && totalCount > maxOccurs) {
- return lookupFDashLocalizations(locale).validatorMaxOccurs(maxOccurs);
+ throw MaxOccursError(nodeUid, maxOccurs);
}
-
- return null;
}
@override
diff --git a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
index e1caa725..20b41ea0 100644
--- a/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
+++ b/lib/questionnaires/model/item/answer/src/coding_answer_option_model.dart
@@ -132,7 +132,7 @@ class CodingAnswerOptionModel {
?.valueString;
final optionPrefix = (plainOptionPrefix != null)
- ? RenderingString.fromText(plainOptionPrefix)
+ ? RenderingString.fromText(plainOptionPrefix, extensions: extensions)
: null;
RenderingString optionText;
@@ -154,6 +154,7 @@ class CodingAnswerOptionModel {
forDisplay = _createForDisplay(coding, locale, questionnaireItemModel);
optionText = RenderingString.fromText(
plainText,
+ extensions: extensions,
);
}
} else {
diff --git a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
index ed56b9bc..541198bd 100644
--- a/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/datetime_answer_model.dart
@@ -1,6 +1,6 @@
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/date_time_error.dart';
import 'package:fhir/r4.dart'
show
Date,
@@ -53,15 +53,15 @@ class DateTimeAnswerModel extends AnswerModel {
}
@override
- String? validateInput(FhirDateTime? inValue) {
+ void validateInput(FhirDateTime? inValue) {
return validateValue(inValue);
}
@override
- String? validateValue(FhirDateTime? inValue) {
- return inValue == null || inValue.isValid
- ? null
- : lookupFDashLocalizations(locale).validatorDateTime;
+ void validateValue(FhirDateTime? inValue) {
+ if (!(inValue == null || inValue.isValid)) {
+ throw DateTimeError(nodeUid);
+ }
}
@override
@@ -81,13 +81,12 @@ class DateTimeAnswerModel extends AnswerModel {
@override
void populate(QuestionnaireResponseAnswer answer) {
// NOTE: Model should probably be populated based on QuestionnaireItemType
- value = answer.valueDateTime ?? (
- (answer.valueDate != null)
- ? FhirDateTime(answer.valueDate)
- : (answer.valueTime != null)
- // TODO: Find a better way to convert Time values to FhirDateTime
- ? FhirDateTime('1970-01-01T${answer.valueTime}')
- : null
- );
+ value = answer.valueDateTime ??
+ ((answer.valueDate != null)
+ ? FhirDateTime(answer.valueDate)
+ : (answer.valueTime != null)
+ // TODO: Find a better way to convert Time values to FhirDateTime
+ ? FhirDateTime('1970-01-01T${answer.valueTime}')
+ : null);
}
}
diff --git a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
index c307d8de..935a76e7 100644
--- a/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/numerical_answer_model.dart
@@ -1,8 +1,11 @@
import 'package:faiadashu/coding/coding.dart';
+import 'package:faiadashu/extensions/function_extension.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/logging/logging.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/max_value_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_value_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/nan_error.dart';
import 'package:fhir/r4.dart';
import 'package:intl/intl.dart';
@@ -161,16 +164,21 @@ class NumericalAnswerModel extends AnswerModel {
}
qi.extension_
- ?.whereExtensionIs('http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption')
- ?.forEach((extension) {
- final coding = extension.valueCoding;
- if (coding == null) return;
- _units[keyForUnitChoice(coding)] = coding;
- });
+ ?.whereExtensionIs(
+ 'http://hl7.org/fhir/StructureDefinition/questionnaire-unitOption')
+ ?.forEach((extension) {
+ final coding = extension.valueCoding;
+ if (coding == null) return;
+ _units[keyForUnitChoice(coding)] = coding;
+ });
// Using updated usage for questionnaire-unit in R5 (http://hl7.org/fhir/extensions/StructureDefinition-questionnaire-unit.html)
- final questionnaireUnit = qi.extension_?.extensionOrNull('http://hl7.org/fhir/StructureDefinition/questionnaire-unit')?.valueCoding;
- if (questionnaireUnit != null && questionnaireUnit.display != null) _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit;
+ final questionnaireUnit = qi.extension_
+ ?.extensionOrNull(
+ 'http://hl7.org/fhir/StructureDefinition/questionnaire-unit')
+ ?.valueCoding;
+ if (questionnaireUnit != null && questionnaireUnit.display != null)
+ _units[keyForUnitChoice(questionnaireUnit)] = questionnaireUnit;
}
@override
@@ -181,9 +189,9 @@ class NumericalAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(String? inputValue) {
+ void validateInput(String? inputValue) {
if (inputValue == null || inputValue.isEmpty) {
- return null;
+ return;
}
num number = double.nan;
try {
@@ -191,17 +199,17 @@ class NumericalAnswerModel extends AnswerModel {
} catch (_) {
// Ignore FormatException, number remains nan.
}
- if (number == double.nan) {
- return lookupFDashLocalizations(locale).validatorNan;
+ if (number.isNaN) {
+ throw NanError(nodeUid);
}
final quantity = _valueFromNumber(number);
- return validateValue(quantity);
+ validateValue(quantity);
}
@override
- String? validateValue(Quantity? inputValue) {
+ void validateValue(Quantity? inputValue) {
if (inputValue == null) {
return null;
}
@@ -213,12 +221,10 @@ class NumericalAnswerModel extends AnswerModel {
}
if (number > _maxValue) {
- return lookupFDashLocalizations(locale)
- .validatorMaxValue(Decimal(_maxValue).format(locale));
+ throw MaxValueError(nodeUid, Decimal(_maxValue).format(locale));
}
if (number < _minValue) {
- return lookupFDashLocalizations(locale)
- .validatorMinValue(Decimal(_minValue).format(locale));
+ throw MinValueError(nodeUid, Decimal(_minValue).format(locale));
}
return null;
@@ -259,7 +265,8 @@ class NumericalAnswerModel extends AnswerModel {
/// * Updates the numerical value based on text input
/// * Keeps the unit
Quantity? copyWithTextInput(String textInput) {
- final valid = validateInput(textInput) == null;
+ final valid = (() => validateInput(textInput)).callSafely() ?? false;
+
final dataAbsentReasonExtension = !valid
? [
FhirExtension(
diff --git a/lib/questionnaires/model/item/answer/src/string_answer_model.dart b/lib/questionnaires/model/item/answer/src/string_answer_model.dart
index d8b11ab0..eb7077a3 100644
--- a/lib/questionnaires/model/item/answer/src/string_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/string_answer_model.dart
@@ -1,7 +1,10 @@
import 'package:faiadashu/coding/coding.dart';
import 'package:faiadashu/fhir_types/fhir_types.dart';
-import 'package:faiadashu/l10n/l10n.dart';
import 'package:faiadashu/questionnaires/model/model.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/entry_format_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/min_length_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/regex_error.dart';
+import 'package:faiadashu/questionnaires/model/src/validation_errors/url_error.dart';
import 'package:fhir/r4.dart';
enum StringAnswerKeyboard { plain, email, phone, number, multiline, url }
@@ -61,73 +64,73 @@ class StringAnswerModel extends AnswerModel {
: RenderingString.nullText;
@override
- String? validateInput(String? inValue) {
+ void validateInput(String? inValue) {
final checkValue = inValue?.trim();
return validateValue(checkValue);
}
@override
- String? validateValue(String? inputValue) {
+ void validateValue(String? inputValue) {
if (inputValue == null || inputValue.isEmpty) {
- return null;
+ return;
}
if (inputValue.length < minLength) {
- return lookupFDashLocalizations(locale).validatorMinLength(minLength);
+ throw MinLengthError(nodeUid, minLength);
}
if (maxLength != null && inputValue.length > maxLength!) {
- return lookupFDashLocalizations(locale).validatorMaxLength(maxLength!);
+ throw MinLengthError(nodeUid, maxLength!);
}
if (qi.type == QuestionnaireItemType.url) {
if (!_urlRegExp.hasMatch(inputValue)) {
- return lookupFDashLocalizations(locale).validatorUrl;
+ throw UrlError(nodeUid);
}
}
if (regExp != null) {
if (!regExp!.hasMatch(inputValue)) {
- return (entryFormat != null)
- ? lookupFDashLocalizations(locale)
- .validatorEntryFormat(entryFormat!)
- : lookupFDashLocalizations(locale).validatorRegExp;
+ throw (entryFormat != null)
+ ? EntryFormatError(nodeUid, entryFormat!)
+ : RegexError(nodeUid);
}
}
-
- return null;
}
@override
QuestionnaireResponseAnswer? createFhirAnswer(
List? items,
) {
- final value = this.value?.trim();
-
- final valid = validateInput(value) == null;
- final dataAbsentReasonExtension = !valid
- ? [
- FhirExtension(
- url: dataAbsentReasonExtensionUrl,
- valueCode: dataAbsentReasonAsTextCode,
- ),
- ]
- : null;
-
- return (value != null && value.isNotEmpty)
- ? (qi.type != QuestionnaireItemType.url)
- ? QuestionnaireResponseAnswer(
- valueString: value,
- extension_: dataAbsentReasonExtension,
- item: items,
- )
- : QuestionnaireResponseAnswer(
- valueUri: FhirUri(value),
- extension_: dataAbsentReasonExtension,
- item: items,
- )
- : null;
+ try {
+ final value = this.value?.trim();
+
+ validateInput(value);
+
+ final dataAbsentReasonExtension = [
+ FhirExtension(
+ url: dataAbsentReasonExtensionUrl,
+ valueCode: dataAbsentReasonAsTextCode,
+ ),
+ ];
+
+ return (value != null && value.isNotEmpty)
+ ? (qi.type != QuestionnaireItemType.url)
+ ? QuestionnaireResponseAnswer(
+ valueString: value,
+ extension_: dataAbsentReasonExtension,
+ item: items,
+ )
+ : QuestionnaireResponseAnswer(
+ valueUri: FhirUri(value),
+ extension_: dataAbsentReasonExtension,
+ item: items,
+ )
+ : null;
+ } on Exception {
+ return null;
+ }
}
@override
diff --git a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
index f16712ef..f37c9e60 100644
--- a/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
+++ b/lib/questionnaires/model/item/answer/src/unsupported_answer_model.dart
@@ -16,12 +16,12 @@ class UnsupportedAnswerModel extends AnswerModel