From 70fba34f0cc727457b196ad3e18d20d1ea98d46e Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 May 2025 22:15:12 +0200 Subject: [PATCH 1/3] Better handling of time zones We now send the current time zone to the server when serializing datetime objects. This was causing entries to be saved some hours wrong or depending on the time, on a different day. --- lib/helpers/date.dart | 40 ++++++++++++++++ lib/helpers/json.dart | 9 ++++ lib/helpers/misc.dart | 32 ------------- lib/models/nutrition/log.dart | 2 +- lib/models/nutrition/log.g.dart | 2 +- lib/models/nutrition/meal.dart | 2 +- lib/models/nutrition/nutritional_plan.dart | 2 +- lib/models/nutrition/nutritional_plan.g.dart | 2 +- lib/models/workouts/log.dart | 2 +- lib/models/workouts/log.g.dart | 2 +- lib/models/workouts/routine.dart | 4 +- lib/models/workouts/routine.g.dart | 2 +- lib/providers/nutrition.dart | 2 +- lib/screens/log_meal_screen.dart | 47 +++++++++++-------- lib/screens/nutritional_plan_screen.dart | 2 +- lib/widgets/dashboard/calendar.dart | 2 +- lib/widgets/dashboard/widgets/nutrition.dart | 2 +- lib/widgets/dashboard/widgets/routines.dart | 2 +- lib/widgets/nutrition/forms.dart | 18 +++---- lib/widgets/nutrition/meal.dart | 2 +- lib/widgets/routines/day.dart | 2 +- lib/widgets/routines/gym_mode/log_page.dart | 19 ++++---- .../routines/gym_mode/session_page.dart | 2 +- lib/widgets/routines/log.dart | 1 + test/exercises/exercise_provider_db_test.dart | 2 +- test/helpers/colors_test.dart | 18 +++++++ .../date_test.dart} | 23 ++++----- test/helpers/json_test.dart | 27 +++++++++++ .../nutritional_meal_item_form_test.dart | 2 +- 29 files changed, 169 insertions(+), 105 deletions(-) create mode 100644 lib/helpers/date.dart rename test/{other/extensions_test.dart => helpers/date_test.dart} (60%) diff --git a/lib/helpers/date.dart b/lib/helpers/date.dart new file mode 100644 index 000000000..9177b8fc6 --- /dev/null +++ b/lib/helpers/date.dart @@ -0,0 +1,40 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (C) wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * wger Workout Manager is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/// Returns a timezone aware DateTime object from a date and time string. +DateTime getDateTimeFromDateAndTime(String date, String time) { + return DateTime.parse('$date $time'); +} + +/// Returns a list of [DateTime] objects from [first] to [last], inclusive. +List daysInRange(DateTime first, DateTime last) { + final dayCount = last.difference(first).inDays + 1; + return List.generate( + dayCount, + (index) => DateTime.utc(first.year, first.month, first.day + index), + ); +} + +extension DateTimeExtension on DateTime { + bool isSameDayAs(DateTime other) { + final thisDay = DateTime(year, month, day); + final otherDay = DateTime(other.year, other.month, other.day); + + return thisDay.isAtSameMomentAs(otherDay); + } +} diff --git a/lib/helpers/json.dart b/lib/helpers/json.dart index f10c1f803..31c9d3965 100644 --- a/lib/helpers/json.dart +++ b/lib/helpers/json.dart @@ -53,6 +53,15 @@ String? dateToYYYYMMDD(DateTime? dateTime) { return DateFormat('yyyy-MM-dd').format(dateTime); } +/// Convert a date to UTC and then to an ISO8601 string. +/// +/// This makes sure that the serialized data has correct timezone information. +/// Otherwise the django backend will possible treat the date as local time, +/// which will not be correct in most cases. +String dateToUtcIso8601(DateTime dateTime) { + return dateTime.toUtc().toIso8601String(); +} + /* * Converts a time to a date object. * Needed e.g. when the wger api only sends a time but no date information. diff --git a/lib/helpers/misc.dart b/lib/helpers/misc.dart index 8db74a856..f99b23642 100644 --- a/lib/helpers/misc.dart +++ b/lib/helpers/misc.dart @@ -63,38 +63,6 @@ String repText( return out.join(' '); } -/// Returns a list of [DateTime] objects from [first] to [last], inclusive. -List daysInRange(DateTime first, DateTime last) { - final dayCount = last.difference(first).inDays + 1; - return List.generate( - dayCount, - (index) => DateTime.utc(first.year, first.month, first.day + index), - ); -} - -extension TimeOfDayExtension on TimeOfDay { - bool isAfter(TimeOfDay other) { - return toMinutes() > other.toMinutes(); - } - - bool isBefore(TimeOfDay other) { - return toMinutes() < other.toMinutes(); - } - - int toMinutes() { - return (hour * 60) + minute; - } -} - -extension DateTimeExtension on DateTime { - bool isSameDayAs(DateTime other) { - final thisDay = DateTime(year, month, day); - final otherDay = DateTime(other.year, other.month, other.day); - - return thisDay.isAtSameMomentAs(otherDay); - } -} - void launchURL(String url, BuildContext context) async { final scaffoldMessenger = ScaffoldMessenger.of(context); final launched = await launchUrl(Uri.parse(url)); diff --git a/lib/models/nutrition/log.dart b/lib/models/nutrition/log.dart index ac4aa8457..d36e5a532 100644 --- a/lib/models/nutrition/log.dart +++ b/lib/models/nutrition/log.dart @@ -36,7 +36,7 @@ class Log { @JsonKey(required: true, name: 'plan') int planId; - @JsonKey(required: true) + @JsonKey(required: true, toJson: dateToUtcIso8601) late DateTime datetime; String? comment; diff --git a/lib/models/nutrition/log.g.dart b/lib/models/nutrition/log.g.dart index dcb1d7218..8d8587d21 100644 --- a/lib/models/nutrition/log.g.dart +++ b/lib/models/nutrition/log.g.dart @@ -27,7 +27,7 @@ Map _$LogToJson(Log instance) => { 'id': instance.id, 'meal': instance.mealId, 'plan': instance.planId, - 'datetime': instance.datetime.toIso8601String(), + 'datetime': dateToUtcIso8601(instance.datetime), 'comment': instance.comment, 'ingredient': instance.ingredientId, 'weight_unit': instance.weightUnitId, diff --git a/lib/models/nutrition/meal.dart b/lib/models/nutrition/meal.dart index 3e98c9e81..b7c51e4d1 100644 --- a/lib/models/nutrition/meal.dart +++ b/lib/models/nutrition/meal.dart @@ -19,8 +19,8 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal_item.dart'; import 'package:wger/models/nutrition/nutritional_values.dart'; diff --git a/lib/models/nutrition/nutritional_plan.dart b/lib/models/nutrition/nutritional_plan.dart index 4318f07e7..3605e2204 100644 --- a/lib/models/nutrition/nutritional_plan.dart +++ b/lib/models/nutrition/nutritional_plan.dart @@ -38,7 +38,7 @@ class NutritionalPlan { @JsonKey(required: true) late String description; - @JsonKey(required: true, name: 'creation_date', toJson: dateToYYYYMMDD) + @JsonKey(required: true, name: 'creation_date', toJson: dateToUtcIso8601) late DateTime creationDate; @JsonKey(required: true, name: 'only_logging') diff --git a/lib/models/nutrition/nutritional_plan.g.dart b/lib/models/nutrition/nutritional_plan.g.dart index cabc3f300..731befbff 100644 --- a/lib/models/nutrition/nutritional_plan.g.dart +++ b/lib/models/nutrition/nutritional_plan.g.dart @@ -37,7 +37,7 @@ NutritionalPlan _$NutritionalPlanFromJson(Map json) { Map _$NutritionalPlanToJson(NutritionalPlan instance) => { 'id': instance.id, 'description': instance.description, - 'creation_date': dateToYYYYMMDD(instance.creationDate), + 'creation_date': dateToUtcIso8601(instance.creationDate), 'only_logging': instance.onlyLogging, 'goal_energy': instance.goalEnergy, 'goal_protein': instance.goalProtein, diff --git a/lib/models/workouts/log.dart b/lib/models/workouts/log.dart index 49a049231..3931d321a 100644 --- a/lib/models/workouts/log.dart +++ b/lib/models/workouts/log.dart @@ -80,7 +80,7 @@ class Log { @JsonKey(includeFromJson: false, includeToJson: false) late WeightUnit? weightUnitObj; - @JsonKey(required: true, toJson: dateToYYYYMMDD) + @JsonKey(required: true, toJson: dateToUtcIso8601) late DateTime date; Log({ diff --git a/lib/models/workouts/log.g.dart b/lib/models/workouts/log.g.dart index 397df1b59..ec4b667cc 100644 --- a/lib/models/workouts/log.g.dart +++ b/lib/models/workouts/log.g.dart @@ -58,5 +58,5 @@ Map _$LogToJson(Log instance) => { 'weight': numToString(instance.weight), 'weight_target': numToString(instance.weightTarget), 'weight_unit': instance.weightUnitId, - 'date': dateToYYYYMMDD(instance.date), + 'date': dateToUtcIso8601(instance.date), }; diff --git a/lib/models/workouts/routine.dart b/lib/models/workouts/routine.dart index d0f55dbba..bd90d6bb3 100644 --- a/lib/models/workouts/routine.dart +++ b/lib/models/workouts/routine.dart @@ -17,8 +17,8 @@ */ import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/models/workouts/day.dart'; import 'package:wger/models/workouts/day_data.dart'; import 'package:wger/models/workouts/log.dart'; @@ -42,7 +42,7 @@ class Routine { @JsonKey(required: true, includeToJson: false) int? id; - @JsonKey(required: true) + @JsonKey(required: true, toJson: dateToUtcIso8601) late DateTime created; @JsonKey(required: true, name: 'name') diff --git a/lib/models/workouts/routine.g.dart b/lib/models/workouts/routine.g.dart index fea9912fa..44649f046 100644 --- a/lib/models/workouts/routine.g.dart +++ b/lib/models/workouts/routine.g.dart @@ -31,7 +31,7 @@ Routine _$RoutineFromJson(Map json) { } Map _$RoutineToJson(Routine instance) => { - 'created': instance.created.toIso8601String(), + 'created': dateToUtcIso8601(instance.created), 'name': instance.name, 'description': instance.description, 'fit_in_week': instance.fitInWeek, diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index b8089f9e6..384890c98 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -421,7 +421,7 @@ class NutritionPlansProvider with ChangeNotifier { ]) async { final plan = findById(planId); mealItem.ingredient = await fetchIngredient(mealItem.ingredientId); - final Log log = Log.fromMealItem(mealItem, plan.id!, null, dateTime); + final log = Log.fromMealItem(mealItem, plan.id!, null, dateTime); final data = await baseProvider.post( log.toJson(), diff --git a/lib/screens/log_meal_screen.dart b/lib/screens/log_meal_screen.dart index 23354206a..0f2566d2d 100644 --- a/lib/screens/log_meal_screen.dart +++ b/lib/screens/log_meal_screen.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/nutrition/meal.dart'; @@ -70,8 +71,10 @@ class _LogMealScreenState extends State { .toList(), ); + final i18n = AppLocalizations.of(context); + return Scaffold( - appBar: AppBar(title: Text(AppLocalizations.of(context).logMeal)), + appBar: AppBar(title: Text(i18n.logMeal)), body: Consumer( builder: (context, nutritionProvider, child) => SingleChildScrollView( child: Padding( @@ -83,7 +86,7 @@ class _LogMealScreenState extends State { style: Theme.of(context).textTheme.headlineSmall, ), if (meal.mealItems.isEmpty) - ListTile(title: Text(AppLocalizations.of(context).noIngredientsDefined)) + ListTile(title: Text(i18n.noIngredientsDefined)) else Column( children: [ @@ -113,7 +116,7 @@ class _LogMealScreenState extends State { child: TextFormField( key: const ValueKey('field-date'), readOnly: true, - decoration: InputDecoration(labelText: AppLocalizations.of(context).date), + decoration: InputDecoration(labelText: i18n.date), enableInteractiveSelection: false, controller: _dateController, onTap: () async { @@ -138,7 +141,7 @@ class _LogMealScreenState extends State { child: TextFormField( key: const ValueKey('field-time'), readOnly: true, - decoration: InputDecoration(labelText: AppLocalizations.of(context).time), + decoration: InputDecoration(labelText: i18n.time), controller: _timeController, onTap: () async { // Open time picker @@ -165,28 +168,32 @@ class _LogMealScreenState extends State { children: [ if (meal.mealItems.isNotEmpty) TextButton( - child: const Text('Log'), + child: Text(i18n.save), onPressed: () async { + final loggedTime = getDateTimeFromDateAndTime( + _dateController.text, + _timeController.text, + ); + await Provider.of( context, listen: false, - ).logMealToDiary( - meal, - DateTime.parse('${_dateController.text} ${_timeController.text}'), - ); - // ignore: use_build_context_synchronously - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - // ignore: use_build_context_synchronously - AppLocalizations.of(context).mealLogged, - textAlign: TextAlign.center, + ).logMealToDiary(meal, loggedTime); + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + i18n.mealLogged, + textAlign: TextAlign.center, + ), ), - ), - ); - Navigator.of(context).pop(); - if (args.popTwice) { + ); + Navigator.of(context).pop(); + if (args.popTwice) { + Navigator.of(context).pop(); + } } }, ), diff --git a/lib/screens/nutritional_plan_screen.dart b/lib/screens/nutritional_plan_screen.dart index c974b06d1..1f8e2062b 100644 --- a/lib/screens/nutritional_plan_screen.dart +++ b/lib/screens/nutritional_plan_screen.dart @@ -60,7 +60,7 @@ class NutritionalPlanScreen extends StatelessWidget { FormScreen.routeName, arguments: FormScreenArguments( AppLocalizations.of(context).logIngredient, - IngredientLogForm(nutritionalPlan), + getIngredientLogForm(nutritionalPlan), hasListView: true, ), ); diff --git a/lib/widgets/dashboard/calendar.dart b/lib/widgets/dashboard/calendar.dart index 2019aa643..ba8ac41c3 100644 --- a/lib/widgets/dashboard/calendar.dart +++ b/lib/widgets/dashboard/calendar.dart @@ -20,8 +20,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/body_weight.dart'; import 'package:wger/providers/measurement.dart'; diff --git a/lib/widgets/dashboard/widgets/nutrition.dart b/lib/widgets/dashboard/widgets/nutrition.dart index ceb4d8bf7..4f7c02811 100644 --- a/lib/widgets/dashboard/widgets/nutrition.dart +++ b/lib/widgets/dashboard/widgets/nutrition.dart @@ -108,7 +108,7 @@ class _DashboardNutritionWidgetState extends State { FormScreen.routeName, arguments: FormScreenArguments( AppLocalizations.of(context).logIngredient, - IngredientLogForm(_plan!), + getIngredientLogForm(_plan!), hasListView: true, ), ); diff --git a/lib/widgets/dashboard/widgets/routines.dart b/lib/widgets/dashboard/widgets/routines.dart index dd34773c2..ae4c06e25 100644 --- a/lib/widgets/dashboard/widgets/routines.dart +++ b/lib/widgets/dashboard/widgets/routines.dart @@ -19,7 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -import 'package:wger/helpers/misc.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/day_data.dart'; import 'package:wger/models/workouts/routine.dart'; diff --git a/lib/widgets/nutrition/forms.dart b/lib/widgets/nutrition/forms.dart index ad5b7aa47..00298b29e 100644 --- a/lib/widgets/nutrition/forms.dart +++ b/lib/widgets/nutrition/forms.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/nutrition/ingredient.dart'; @@ -113,7 +114,7 @@ class MealForm extends StatelessWidget { } } -Widget MealItemForm( +Widget getMealItemForm( Meal meal, List recent, [ String? barcode, @@ -132,7 +133,7 @@ Widget MealItemForm( ); } -Widget IngredientLogForm(NutritionalPlan plan) { +Widget getIngredientLogForm(NutritionalPlan plan) { return IngredientForm( recent: plan.dedupDiaryEntries, onSave: (BuildContext context, MealItem mealItem, DateTime? dt) { @@ -394,16 +395,11 @@ class IngredientFormState extends State { _form.currentState!.save(); _mealItem.ingredientId = int.parse(_ingredientIdController.text); - var date = DateTime.parse(_dateController.text); - final tod = stringToTime(_timeController.text); - date = DateTime( - date.year, - date.month, - date.day, - tod.hour, - tod.minute, + final loggedDate = getDateTimeFromDateAndTime( + _dateController.text, + _timeController.text, ); - widget.onSave(context, _mealItem, date); + widget.onSave(context, _mealItem, loggedDate); Navigator.of(context).pop(); }, diff --git a/lib/widgets/nutrition/meal.dart b/lib/widgets/nutrition/meal.dart index 002961095..3aebb1608 100644 --- a/lib/widgets/nutrition/meal.dart +++ b/lib/widgets/nutrition/meal.dart @@ -116,7 +116,7 @@ class _MealWidgetState extends State { FormScreen.routeName, arguments: FormScreenArguments( AppLocalizations.of(context).addIngredient, - MealItemForm(widget._meal, widget._recentMealItems), + getMealItemForm(widget._meal, widget._recentMealItems), hasListView: true, ), ); diff --git a/lib/widgets/routines/day.dart b/lib/widgets/routines/day.dart index 5c5447081..46bc5e3c3 100644 --- a/lib/widgets/routines/day.dart +++ b/lib/widgets/routines/day.dart @@ -17,7 +17,7 @@ */ import 'package:flutter/material.dart'; -import 'package:wger/helpers/misc.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/day_data.dart'; diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 17ace6e74..48b486aa2 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -298,15 +298,18 @@ class _LogPageState extends State { context, listen: false, ).addLog(widget._log); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 2), // default is 4 - content: Text( - AppLocalizations.of(context).successfullySaved, - textAlign: TextAlign.center, + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), // default is 4 + content: Text( + AppLocalizations.of(context).successfullySaved, + textAlign: TextAlign.center, + ), ), - ), - ); + ); + } widget._controller.nextPage( duration: DEFAULT_ANIMATION_DURATION, curve: DEFAULT_ANIMATION_CURVE, diff --git a/lib/widgets/routines/gym_mode/session_page.dart b/lib/widgets/routines/gym_mode/session_page.dart index ce9c0f90f..0a22a8e6e 100644 --- a/lib/widgets/routines/gym_mode/session_page.dart +++ b/lib/widgets/routines/gym_mode/session_page.dart @@ -18,7 +18,7 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; import 'package:wger/helpers/consts.dart'; -import 'package:wger/helpers/misc.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/workouts/routine.dart'; diff --git a/lib/widgets/routines/log.dart b/lib/widgets/routines/log.dart index 3028341fe..25ce8d244 100644 --- a/lib/widgets/routines/log.dart +++ b/lib/widgets/routines/log.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:wger/helpers/colors.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/errors.dart'; import 'package:wger/helpers/misc.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; diff --git a/test/exercises/exercise_provider_db_test.dart b/test/exercises/exercise_provider_db_test.dart index 5723bab26..1b092bf8b 100644 --- a/test/exercises/exercise_provider_db_test.dart +++ b/test/exercises/exercise_provider_db_test.dart @@ -8,7 +8,7 @@ import 'package:shared_preferences_platform_interface/in_memory_shared_preferenc import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; import 'package:wger/database/exercises/exercise_database.dart'; import 'package:wger/helpers/consts.dart'; -import 'package:wger/helpers/misc.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/shared_preferences.dart'; import 'package:wger/models/exercises/exercise_api.dart'; import 'package:wger/models/exercises/muscle.dart'; diff --git a/test/helpers/colors_test.dart b/test/helpers/colors_test.dart index da451a787..6dc2e6c7d 100644 --- a/test/helpers/colors_test.dart +++ b/test/helpers/colors_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (C) wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * wger Workout Manager is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:wger/helpers/colors.dart'; diff --git a/test/other/extensions_test.dart b/test/helpers/date_test.dart similarity index 60% rename from test/other/extensions_test.dart rename to test/helpers/date_test.dart index c3140fcae..49eed7060 100644 --- a/test/other/extensions_test.dart +++ b/test/helpers/date_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (C) wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -16,21 +16,16 @@ * along with this program. If not, see . */ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:wger/helpers/misc.dart'; +import 'package:wger/helpers/date.dart'; void main() { - test('Test the TimeOfDayExtension', () { - const time1 = TimeOfDay(hour: 00, minute: 00); - const time2 = TimeOfDay(hour: 23, minute: 59); - - expect(time2.toMinutes(), 23 * 60 + 59); - expect(time1.isAfter(time2), false); - expect(time1.isBefore(time2), true); - expect(time2.isAfter(time1), true); - expect(time2.isBefore(time1), false); - expect(time1.isAfter(time1), false); - expect(time2.isBefore(time2), false); + group('getDateTimeFromDateAndTime', () { + test('should correctly generate a DateTime', () { + expect( + getDateTimeFromDateAndTime('2025-05-16', '17:02'), + DateTime(2025, 5, 16, 17, 2), + ); + }); }); } diff --git a/test/helpers/json_test.dart b/test/helpers/json_test.dart index c0ac1d8c5..098d8e67d 100644 --- a/test/helpers/json_test.dart +++ b/test/helpers/json_test.dart @@ -1,3 +1,21 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (C) wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * wger Workout Manager is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:wger/helpers/json.dart'; @@ -39,6 +57,15 @@ void main() { }); }); + group('dateToIsoWithTimezone', () { + test('should format DateTime to a string with timezone', () { + expect( + dateToUtcIso8601(DateTime.parse('2025-05-16T18:15:00+02:00')), + '2025-05-16T16:15:00.000Z', + ); + }); + }); + group('stringToTime', () { test('should default to 00:00 for null input', () { expect(stringToTime(null), const TimeOfDay(hour: 0, minute: 0)); diff --git a/test/nutrition/nutritional_meal_item_form_test.dart b/test/nutrition/nutritional_meal_item_form_test.dart index 1dbd28b06..017d9a171 100644 --- a/test/nutrition/nutritional_meal_item_form_test.dart +++ b/test/nutrition/nutritional_meal_item_form_test.dart @@ -101,7 +101,7 @@ void main() { home: Scaffold( body: Scrollable( viewportBuilder: (BuildContext context, ViewportOffset position) => - MealItemForm(meal, const [], code, test), + getMealItemForm(meal, const [], code, test), ), ), routes: { From 99580490d3ad4bbebf4aeff387546b5a0a8e7e8e Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 28 Aug 2025 22:25:11 +0200 Subject: [PATCH 2/3] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/helpers/json.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers/json.dart b/lib/helpers/json.dart index 31c9d3965..1ac50ffb5 100644 --- a/lib/helpers/json.dart +++ b/lib/helpers/json.dart @@ -56,7 +56,7 @@ String? dateToYYYYMMDD(DateTime? dateTime) { /// Convert a date to UTC and then to an ISO8601 string. /// /// This makes sure that the serialized data has correct timezone information. -/// Otherwise the django backend will possible treat the date as local time, +/// Otherwise the django backend will possibly treat the date as local time, /// which will not be correct in most cases. String dateToUtcIso8601(DateTime dateTime) { return dateTime.toUtc().toIso8601String(); From fa96378815549540831a597c840aeb5a5d8ba37a Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Thu, 28 Aug 2025 22:35:29 +0200 Subject: [PATCH 3/3] Import datetime extension --- lib/widgets/weight/forms.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/weight/forms.dart b/lib/widgets/weight/forms.dart index 5b309e3f3..d91cdbeac 100644 --- a/lib/widgets/weight/forms.dart +++ b/lib/widgets/weight/forms.dart @@ -21,8 +21,8 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/json.dart'; -import 'package:wger/helpers/misc.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/body_weight/weight_entry.dart'; import 'package:wger/providers/body_weight.dart';