Skip to content

Commit

Permalink
Merge pull request #545 from hpi-studyu/feat/free-text
Browse files Browse the repository at this point in the history
Feat/free text
  • Loading branch information
johannesvedder committed Jan 24, 2024
2 parents 5dc2cd1 + ebf377b commit 8278e67
Show file tree
Hide file tree
Showing 63 changed files with 11,067 additions and 9,173 deletions.
33 changes: 27 additions & 6 deletions app/lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"get_started": "Los geht's",
"test": "test10",
"study_selection": "Studienauswahl",
"study_selection_description": "Bitte wähle eine Studie aus.",
"study_selection_description": "Bitte wählen Sie eine Studie aus.",
"study_selection_single": "Sie können zu jeder Zeit maximal an einer Studie teilnehmen.",
"study_selection_single_why": "Warum?",
"study_selection_single_reason": "Wenn Sie zur selben Zeit an mehreren Studien teilnehmen würde, könnten die Kombination der Interventionen die Ergebnisse verfälschen.",
Expand All @@ -30,7 +30,7 @@
"no_interventions_available": "Keine Interventionen verfügbar.",
"loading_interventions": "Interventionen laden",
"task_cannot_be_completed": "Die Aufgabe kann nicht bearbeitet werden",
"study_notification_body": "Eine neue Aufgabe benötigt deine Aufmerksamkeit",
"study_notification_body": "Eine neue Aufgabe benötigt Ihre Aufmerksamkeit",

"intervention_phase_duration": "Länge der Interventionphasen",
"days": "Tage",
Expand All @@ -46,7 +46,7 @@
"intervention_current": "Aktuelle Maßnahme",
"study_current": "Aktuelle Studie:",
"opt_out": "Aussteigen",
"delete_data": "Aussteigen & Daten löschen",
"delete_data": "Aussteigen und Daten löschen",

"summary": "Übersicht",
"consent": "Einverständnis",
Expand Down Expand Up @@ -155,8 +155,8 @@

"report_axis_phase": "Phase",

"study_not_started": "Deine Studie hat noch nicht angefangen. Bitte schaue morgen noch einmal vorbei!",
"completed_study": "Du hast deine letzte Studie abgeschlossen. Schaue dir vergangene Ergebnisse an oder starte eine neue Studie.",
"study_not_started": "Ihre Studie hat noch nicht angefangen. Bitte schauen Sie morgen noch einmal vorbei!",
"completed_study": "Sie haben Ihre letzte Studie abgeschlossen. Schauen Sie vergangene Ergebnisse an oder starten Sie eine neue Studie.",

"app_support": "App Support",
"app_support_text": "Bei Problemen oder Fragen zur App kontaktieren",
Expand All @@ -168,5 +168,26 @@
"website": "Website",
"email": "Email",
"phone": "Telefon",
"additionalInfo": "Zusätzliche Informationen"
"additionalInfo": "Zusätzliche Informationen",

"free_text_min_length_error": "Bitte geben Sie mindestens {min} Zeichen ein",
"@free_text_min_length_error": {
"placeholders": {
"min": {}
}
},
"free_text_max_length_error": "Bitte geben Sie maximal {max} Zeichen ein",
"@free_text_max_length_error": {
"placeholders": {
"max": {}
}
},
"free_text_alphanumeric_error": "Bitte geben Sie nur alphanumerische Zeichen ein",
"free_text_numeric_error": "Bitte geben Sie nur numerische Zeichen ein",
"free_text_custom_error": "Bitte geben Sie nur Zeichen ein, die dem Muster {pattern} entsprechen",
"@free_text_custom_error": {
"placeholders": {
"pattern": {}
}
}
}
23 changes: 22 additions & 1 deletion app/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,26 @@
"website": "Website",
"email": "Email",
"phone": "Phone",
"additionalInfo": "Additional information"
"additionalInfo": "Additional information",

"free_text_min_length_error": "Please enter at least {min} characters",
"@free_text_min_length_error": {
"placeholders": {
"min": {}
}
},
"free_text_max_length_error": "Please enter at most {max} characters",
"@free_text_max_length_error": {
"placeholders": {
"max": {}
}
},
"free_text_alphanumeric_error": "Please enter only alphanumeric characters",
"free_text_numeric_error": "Please enter only numeric characters",
"free_text_custom_error": "Please enter only characters matching the pattern {pattern}",
"@free_text_custom_error": {
"placeholders": {
"pattern": {}
}
}
}
23 changes: 11 additions & 12 deletions app/lib/screens/app_onboarding/loading_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ class _LoadingScreenState extends State<LoadingScreen> {

Future<void> initStudy() async {
final state = context.read<AppState>();
Analytics.init();

if (widget.queryParameters != null && widget.queryParameters!.isNotEmpty) {
Analytics.logger.info("Preview: Found query parameters ${widget.queryParameters}");
StudyULogger.info("Preview: Found query parameters ${widget.queryParameters}");
var lang = context.watch<AppLanguage>();
final preview = Preview(
widget.queryParameters,
Expand Down Expand Up @@ -161,7 +160,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
}

final selectedStudyObjectId = await getActiveSubjectId();
Analytics.logger.info('Subject ID: $selectedStudyObjectId');
StudyULogger.info('Subject ID: $selectedStudyObjectId');
state.analytics.initBasic();
if (!mounted) return;
if (selectedStudyObjectId == null) {
Expand All @@ -170,7 +169,8 @@ class _LoadingScreenState extends State<LoadingScreen> {
Navigator.pushReplacementNamed(context, Routes.studySelection);
return;
}*/
Analytics.addBreadcrumb(category: 'waypoint', message: 'No subject ID found and not logged in -> welcome');
StudyUDiagnostics.addBreadcrumb(
category: 'waypoint', message: 'No subject ID found and not logged in -> welcome');
Navigator.pushReplacementNamed(context, Routes.welcome);
return;
}
Expand All @@ -180,13 +180,13 @@ class _LoadingScreenState extends State<LoadingScreen> {
try {
InternetAddress.lookup(Uri.parse(supabaseUrl).host);
} on SocketException catch (_) {
Analytics.logger.warning('Could not connect to supabase url. Fallback to offline mode');
StudyULogger.warning('Could not connect to supabase url. Fallback to offline mode');
subject = await Cache.loadSubject();
}
*/
final connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
Analytics.logger.warning("Could not find any connection. Going to offline mode");
StudyULogger.warning("Could not find any connection. Going to offline mode");
subject = await Cache.loadSubject();
}
subject ??= await SupabaseQuery.getById<StudySubject>(
Expand All @@ -198,8 +198,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
],
);
} catch (exception) {
Analytics.logger
.warning("Could not retrieve subject, maybe JWT is expired, try logging in: ${exception.toString()}");
StudyULogger.warning("Could not retrieve subject, maybe JWT is expired, try logging in: ${exception.toString()}");
/*await Analytics.captureEvent(
SentryEvent(throwable: exception),
stackTrace: stackTrace,
Expand Down Expand Up @@ -229,7 +228,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
// 5. Restart the app. Either only this error shows up, worst case is
// app hangs and is unresponsive

Analytics.logger.warning('Could not login and retrieve the study subject.'
StudyULogger.warning('Could not login and retrieve the study subject.'
'One reason for this might be that the study subject is no '
'longer available and only resides in app backup');
/*await Analytics.captureEvent(
Expand All @@ -238,8 +237,8 @@ class _LoadingScreenState extends State<LoadingScreen> {
);*/
// subject = await Cache.loadSubject();
} catch (exception, stackTrace) {
Analytics.logger.severe('Error when initializing offline mode: ${exception.toString()}');
await Analytics.captureException(
StudyULogger.fatal('Error when initializing offline mode: ${exception.toString()}');
await StudyUDiagnostics.captureException(
exception,
stackTrace: stackTrace,
);
Expand Down Expand Up @@ -268,7 +267,7 @@ class _LoadingScreenState extends State<LoadingScreen> {
state.init(context);
Navigator.pushReplacementNamed(context, Routes.dashboard);
} else {
Analytics.logger.severe('Subject is null -> welcome');
StudyULogger.fatal('Subject is null -> welcome');
Navigator.pushReplacementNamed(context, Routes.welcome);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class ContactItem extends StatelessWidget {
if (await canLaunchUrl(uri)) {
launchUrl(uri);
} else {
Analytics.logger.warning("Cannot launch Url: $uri");
StudyULogger.warning("Cannot launch Url: $uri");
}
}
}
Expand Down
28 changes: 26 additions & 2 deletions app/lib/screens/study/onboarding/eligibility_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class EligibilityScreen extends StatefulWidget {

class _EligibilityScreenState extends State<EligibilityScreen> {
EligibilityResult? activeResult;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();

@override
void initState() {
Expand All @@ -47,7 +48,10 @@ class _EligibilityScreenState extends State<EligibilityScreen> {

bool _checkContinuation(QuestionnaireState qs) {
final criteria = widget.study!.eligibilityCriteria;
final failingResult = criteria.firstWhereOrNull((element) => element.isViolated(qs));
EligibilityCriterion? failingResult = criteria.firstWhereOrNull((element) => element.isViolated(qs));
// freetext quickfix start
failingResult = _isFreeTextCriterion(failingResult!) ? null : failingResult;
// freetext quickfix end
if (failingResult == null) return true;
setState(() {
activeResult = EligibilityResult(qs, eligible: false, firstFailed: failingResult);
Expand All @@ -58,7 +62,14 @@ class _EligibilityScreenState extends State<EligibilityScreen> {
void _evaluateResponse(QuestionnaireState qs) {
final criteria = widget.study!.eligibilityCriteria;
setState(() {
final conditionResult = criteria.every((criterion) => criterion.isSatisfied(qs));
final conditionResult = criteria.every((criterion) {
// freetext quickfix start
if (_isFreeTextCriterion(criterion)) {
return true;
}
// freetext quickfix end
return criterion.isSatisfied(qs);
});
if (conditionResult) {
activeResult = EligibilityResult(qs, eligible: conditionResult);
} else {
Expand All @@ -68,6 +79,19 @@ class _EligibilityScreenState extends State<EligibilityScreen> {
});
}

// todo quickfix until other question types are implemented (see DesignerV2's QuestionFormData)
// make all free text questions eligible
bool _isFreeTextCriterion(EligibilityCriterion criterion) {
return widget.study?.questionnaire.questions.any((element) {
if (criterion.condition.type == ChoiceExpression.expressionType) {
ChoiceExpression choiceExpression = criterion.condition as ChoiceExpression;
return element.id == choiceExpression.target!;
}
return false;
}) ??
false;
}

void _finish() {
Navigator.pop(context, activeResult);
}
Expand Down
6 changes: 3 additions & 3 deletions app/lib/screens/study/onboarding/kickoff.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class _KickoffScreen extends State<KickoffScreen> {
setState(() => ready = true);
Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (_) => false);
} catch (e) {
print('Failed creating subject: $e');
StudyULogger.fatal('Failed creating subject: $e');
}
}

Expand Down Expand Up @@ -83,10 +83,10 @@ class _KickoffScreen extends State<KickoffScreen> {
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 16),
OutlinedButton(
/*OutlinedButton(
onPressed: () => _storeUserStudy(context),
child: Text(AppLocalizations.of(context)!.start_study),
),
),*/
],
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class _QuestionnaireTaskWidgetState extends State<QuestionnaireTaskWidget> {
late bool responseValidator;
DateTime? loginClickTime;
bool _isLoading = false;
final GlobalKey<FormState> formKey = GlobalKey<FormState>();

Future<void> _addQuestionnaireResult<T>(T response, BuildContext context) async {
await handleTaskCompletion(context, (StudySubject? subject) async {
Expand Down Expand Up @@ -52,7 +53,10 @@ class _QuestionnaireTaskWidgetState extends State<QuestionnaireTaskWidget> {
child: Column(
children: [
Expanded(
child: questionnaireWidget,
child: Form(
key: formKey,
child: questionnaireWidget,
),
),
if (response != null && responseValidator)
ElevatedButton.icon(
Expand All @@ -61,6 +65,9 @@ class _QuestionnaireTaskWidgetState extends State<QuestionnaireTaskWidget> {
if (isRedundantClick(loginClickTime)) {
return;
}
if (!formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
Expand Down
10 changes: 5 additions & 5 deletions app/lib/screens/study/tasks/task_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ handleTaskCompletion(BuildContext context, Function(StudySubject?) completionCal
try {
if (state.trackParticipantProgress) {
await completionCallback(activeSubject);
Analytics.logger.info("Saved results in online mode");
debugPrint("Saved results in online mode");
}
} on SocketException catch (exception, stackTrace) {
Analytics.logger.info("Saving results in offline mode");
Analytics.captureEvent(exception, stackTrace: stackTrace);
debugPrint("Saving results in offline mode");
StudyUDiagnostics.captureEvent(exception, stackTrace: stackTrace);
await Cache.storeSubject(activeSubject);
} catch (exception, stackTrace) {
Analytics.logger.severe("Could not save results");
Analytics.captureException(exception, stackTrace: stackTrace);
debugPrint("Could not save results");
StudyUDiagnostics.captureException(exception, stackTrace: stackTrace);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
Expand Down
22 changes: 11 additions & 11 deletions app/lib/util/cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ class Cache {
if (subject == null) return;
StudySubject newSubject;
newSubject = await synchronize(subject);
Analytics.logger.info("Save to local cache");
debugPrint("Save to local cache");
(await sharedPrefs).setString(cacheSubjectKey, jsonEncode(newSubject.toFullJson()));
assert(newSubject == (await loadSubject()));
}

static Future<StudySubject> loadSubject() async {
Analytics.logger.info("Load subject from cache");
debugPrint("Load subject from cache");
if ((await sharedPrefs).containsKey(cacheSubjectKey)) {
return StudySubject.fromJson(jsonDecode((await sharedPrefs).getString(cacheSubjectKey)!));
} else {
Analytics.logger.warning("No cached subject found");
StudyULogger.warning("No cached subject found");
throw Exception("No cached subject found");
}
}
Expand All @@ -41,7 +41,7 @@ class Cache {
}

static Future<void> delete() async {
Analytics.logger.warning("Delete cache");
StudyULogger.warning("Delete cache");
(await sharedPrefs).remove(cacheSubjectKey);
}

Expand All @@ -55,14 +55,14 @@ class Cache {
// remote subject has newer study
if (!kDebugMode && remoteSubject.startedAt!.isAfter(localSubject.startedAt!)) return remoteSubject;

Analytics.logger.info("Synchronize with cache");
debugPrint("Synchronize with cache");
isSynchronizing = true;

try {
// only minimal update
// Check if progress has changed
if (localSubject.progress.length != remoteSubject.progress.length) {
Analytics.logger.info("[Sync] Found different progress length");
StudyULogger.info("Cache found different progress length");
/*if (remoteSubject.progress.isNotEmpty) {
// sort remote progress list from oldest to newest
remoteSubject.progress.sort((a, b) =>
Expand Down Expand Up @@ -94,16 +94,16 @@ class Cache {
// We can either drop local or overwrite remote
// ... for now do nothing
if (!kDebugMode && localSubject.startedAt == remoteSubject.startedAt) {
Analytics.logger.severe("Cache synchronization found local changes that cannot be merged");
Analytics.captureMessage(
StudyULogger.fatal("Cache synchronization found local changes that cannot be merged");
StudyUDiagnostics.captureMessage(
"localSubject: ${localSubject.toFullJson()} \nremoteSubject: ${remoteSubject.toFullJson()}");
Analytics.captureException(Exception("CacheSynchronizationException"));
StudyUDiagnostics.captureException(Exception("CacheSynchronizationException"));
}
}
} on SocketException catch (_) {
Analytics.logger.info("SocketException on synchronizing (normal if offline)");
StudyULogger.info("SocketException on synchronizing (normal if offline)");
} catch (exception) {
Analytics.logger.warning(exception);
StudyULogger.warning(exception);
}
isSynchronizing = false;
return remoteSubject;
Expand Down
Loading

0 comments on commit 8278e67

Please sign in to comment.