diff --git a/lib/services/database_mutation_functions.dart b/lib/services/database_mutation_functions.dart index 48c1ea5a7..583a523b0 100644 --- a/lib/services/database_mutation_functions.dart +++ b/lib/services/database_mutation_functions.dart @@ -6,6 +6,7 @@ import 'package:talawa/locator.dart'; import 'package:talawa/models/organization/org_info.dart'; import 'package:talawa/utils/post_queries.dart'; import 'package:talawa/utils/queries.dart'; +import 'package:talawa/utils/time_conversion.dart'; /// DataBaseMutationFunctions class provides different services that are under the context of graphQL mutations and queries. /// @@ -97,6 +98,13 @@ class DataBaseMutationFunctions { return await gqlAuthQuery(query, variables: variables); } } else if (result.data != null && result.isConcrete) { + // coverage:ignore-start + traverseAndConvertDates( + result.data ?? {}, + convertUTCToLocal, + splitDateTimeLocal, + ); + // coverage:ignore-end return result; } return noData; @@ -117,6 +125,11 @@ class DataBaseMutationFunctions { String mutation, { Map? variables, }) async { + // coverage:ignore-start + if (variables != null) { + traverseAndConvertDates(variables, convertLocalToUTC, splitDateTimeUTC); + } + // coverage:ignore-end final MutationOptions options = MutationOptions( document: gql(mutation), variables: variables ?? {}, @@ -157,6 +170,11 @@ class DataBaseMutationFunctions { Map? variables, bool reCall = true, }) async { + // coverage:ignore-start + if (variables != null) { + traverseAndConvertDates(variables, convertLocalToUTC, splitDateTimeUTC); + } + // coverage:ignore-end final MutationOptions options = MutationOptions( document: gql(mutation), variables: variables ?? {}, @@ -209,6 +227,13 @@ class DataBaseMutationFunctions { result.exception!, ); } else if (result.data != null && result.isConcrete) { + // coverage:ignore-start + traverseAndConvertDates( + result.data ?? {}, + convertUTCToLocal, + splitDateTimeLocal, + ); + // coverage:ignore-end return result; } return noData; diff --git a/lib/utils/time_conversion.dart b/lib/utils/time_conversion.dart new file mode 100644 index 000000000..10e0b16a3 --- /dev/null +++ b/lib/utils/time_conversion.dart @@ -0,0 +1,138 @@ +import 'package:intl/intl.dart'; + +/// Combines the given date and time strings into a single string. +/// +/// **params**: +/// * `date`: The date string in a valid date format (e.g., 'YYYY-MM-DD'). +/// * `time`: The time string in a valid time format (e.g., 'HH:MM:SS'). +/// +/// **returns**: +/// * `String`: A string that combines the `date` and `time`, separated by a space. +String combineDateTime(String date, String time) { + return '$date $time'; +} + +/// Splits the given UTC date and time string into separate date and time strings. +/// +/// **params**: +/// * `dateTimeStr`: The UTC date and time string in a valid format. +/// +/// **returns**: +/// * `Map`: A map containing the separate date and time strings. +Map splitDateTimeUTC(String dateTimeStr) { + final DateTime dateTime = DateTime.parse(dateTimeStr); + return { + 'date': DateFormat('yyyy-MM-dd').format(dateTime), + 'time': DateFormat("HH:mm:ss.SSS'Z'").format(dateTime), + }; +} + +/// Splits the given local date and time string into separate date and time strings. +/// +/// **params**: +/// * `dateTimeStr`: The local date and time string in a valid format. +/// +/// **returns**: +/// * `Map`: A map containing the separate date and time strings. +Map splitDateTimeLocal(String dateTimeStr) { + final DateTime dateTime = DateTime.parse(dateTimeStr); + return { + 'date': DateFormat('yyyy-MM-dd').format(dateTime), + 'time': DateFormat('HH:mm').format(dateTime), + }; +} + +/// Converts the given UTC time to local time. +/// +/// **params**: +/// * `utcTime`: The UTC time string in a valid format. +/// +/// **returns**: +/// * `String`: The converted local time string. +String convertUTCToLocal(String utcTime) { + final DateTime dateTime = DateTime.parse(utcTime).toLocal(); + return DateFormat('yyyy-MM-ddTHH:mm:ss.SSS').format(dateTime); +} + +/// Converts the given local time to UTC time. +/// +/// **params**: +/// * `localTime`: The local time string in a valid format. +/// +/// **returns**: +/// * `String`: The converted UTC time string. +String convertLocalToUTC(String localTime) { + final DateTime dateTime = DateTime.parse(localTime).toUtc(); + return DateFormat("yyyy-MM-ddTHH:mm:ss.SSS'Z'").format(dateTime); +} + +/// Traverses a nested map and converts date and time fields to the desired format. +/// +/// **params**: +/// * `obj`: The nested map to traverse and convert. +/// * `convertFn`: A function that converts a combined date and time string to the desired format. +/// * `splitFn`: A function that splits a converted date and time string into separate date and time strings. +/// +/// **returns**: +/// None +void traverseAndConvertDates( + Map obj, + String Function(String) convertFn, + Map Function(String) splitFn, +) { + obj.forEach((key, value) { + final pairedFields = + dateTimeFields['pairedFields']?.cast>(); + if (pairedFields != null) { + for (final field in pairedFields) { + if (key == field['dateField'] && obj.containsKey(field['timeField'])) { + final combinedDateTime = combineDateTime( + obj[field['dateField']] as String, + obj[field['timeField']] as String, + ); + + final convertedDateTime = convertFn(combinedDateTime); + + final splitDateTime = splitFn(convertedDateTime); + + obj[field['dateField'] ?? ''] = splitDateTime['date'] ?? ''; + obj[field['timeField'] ?? ''] = splitDateTime['time'] ?? ''; + } + } + } + + if (dateTimeFields['directFields']?.cast().contains(key) ?? false) { + obj[key] = convertFn(value as String); + } + + if (value is Map) { + traverseAndConvertDates(value, convertFn, splitFn); + } else if (value is List) { + for (final item in value) { + if (item is Map) { + traverseAndConvertDates(item, convertFn, splitFn); + } + } + } + }); +} + +/// Contains information about the date and time fields used for conversion. +const dateTimeFields = { + 'directFields': [ + 'createdAt', + 'birthDate', + 'updatedAt', + 'recurrenceStartDate', + 'recurrenceEndDate', + 'pluginCreatedBy', + 'dueDate', + 'completionDate', + 'startCursor', + 'endCursor', + ], + 'pairedFields': [ + {'dateField': 'startDate', 'timeField': 'startTime'}, + {'dateField': 'endDate', 'timeField': 'endTime'}, + ], +}; diff --git a/lib/view_model/after_auth_view_models/event_view_models/create_event_view_model.dart b/lib/view_model/after_auth_view_models/event_view_models/create_event_view_model.dart index 88bb6ad80..6d7f44371 100644 --- a/lib/view_model/after_auth_view_models/event_view_models/create_event_view_model.dart +++ b/lib/view_model/after_auth_view_models/event_view_models/create_event_view_model.dart @@ -225,12 +225,10 @@ class CreateEventViewModel extends BaseModel { 'organizationId': _currentOrg.id, 'startDate': DateFormat('yyyy-MM-dd').format(eventStartDate), 'endDate': DateFormat('yyyy-MM-dd').format(eventEndDate), - 'startTime': isAllDay - ? null - : '${DateFormat('HH:mm:ss').format(startTime)}Z', - 'endTime': isAllDay - ? null - : '${DateFormat('HH:mm:ss').format(endTime)}Z', + 'startTime': + isAllDay ? null : DateFormat('HH:mm:ss').format(startTime), + 'endTime': + isAllDay ? null : DateFormat('HH:mm:ss').format(endTime), }, if (isRecurring) 'recurrenceRuleData': { diff --git a/pubspec.lock b/pubspec.lock index 978d19581..d117a4486 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -178,7 +178,7 @@ packages: source: hosted version: "0.4.1" clock: - dependency: transitive + dependency: "direct main" description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf diff --git a/pubspec.yaml b/pubspec.yaml index 1afc201d1..10dae5c4e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: auto_size_text: ^3.0.0 cached_network_image: ^3.4.1 + clock: ^1.1.1 connectivity_plus: ^5.0.2 contained_tab_bar_view: ^0.8.0 diff --git a/test/service_tests/event_service_test.dart b/test/service_tests/event_service_test.dart index 9697175e1..665108347 100644 --- a/test/service_tests/event_service_test.dart +++ b/test/service_tests/event_service_test.dart @@ -23,6 +23,7 @@ void main() { testSetupLocator(); registerServices(); }); + group('Test EventService', () { test('Test editEvent method', () async { final dataBaseMutationFunctions = locator(); @@ -118,7 +119,7 @@ void main() { (realInvocation) async => QueryResult( options: QueryOptions(document: gql(query)), data: { - 'cretedEvent': { + 'createdEvent': { '_id': 'eventId', 'title': 'Test task', 'description': 'Test description', @@ -137,12 +138,13 @@ void main() { when( dataBaseMutationFunctions.gqlAuthMutation( EventQueries().registerForEvent(), + variables: {'eventId': 'eventId'}, ), ).thenAnswer( (realInvocation) async => QueryResult( options: QueryOptions(document: gql(query)), data: { - 'register for an event': { + 'registerForEvent': { '_id': 'eventId', }, }, @@ -250,6 +252,7 @@ void main() { final model = EventService(); expect(model.eventStream, isA>>()); }); + test('Test createVolunteerGroup method', () async { final dataBaseMutationFunctions = locator(); const query = ''; diff --git a/test/widget_tests/after_auth_screens/events/time_conversion_test.dart b/test/widget_tests/after_auth_screens/events/time_conversion_test.dart new file mode 100644 index 000000000..7b5eb3ec8 --- /dev/null +++ b/test/widget_tests/after_auth_screens/events/time_conversion_test.dart @@ -0,0 +1,129 @@ +import 'package:clock/clock.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:talawa/utils/time_conversion.dart'; + +import '../../../helpers/test_helpers.dart'; + +void main() { + group('Time Conversion Utils', () { + setUp(() { + registerServices(); + }); + + tearDown(() { + unregisterServices(); + }); + + test('combineDateTime combines date and time correctly', () { + expect(combineDateTime('2023-05-01', '14:30:00'), '2023-05-01 14:30:00'); + }); + + test('splitDateTimeUTC splits UTC datetime correctly', () { + final result = splitDateTimeUTC('2023-05-01T14:30:00.000Z'); + expect(result['date'], '2023-05-01'); + expect(result['time'], '14:30:00.000Z'); + }); + + test('splitDateTimeLocal splits local datetime correctly', () { + final result = splitDateTimeLocal('2023-05-01T14:30:00.000'); + expect(result['date'], '2023-05-01'); + expect(result['time'], '14:30'); + }); + + test('convertUTCToLocal converts UTC to local time', () { + const utcTime = '2023-05-01T14:30:00.000Z'; + final localTime = convertUTCToLocal(utcTime); + expect(localTime, isNot(equals(utcTime))); + expect( + localTime, + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$'), + ); + }); + + test('convertLocalToUTC converts local to UTC time', () { + const localTime = '2023-05-01T14:30:00.000'; + final utcTime = convertLocalToUTC(localTime); + expect(utcTime, isNot(equals(localTime))); + expect( + utcTime, + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$'), + ); + }); + + group('traverseAndConvertDates', () { + test('converts direct fields', () { + final testObj = { + 'createdAt': '2023-05-01T14:30:00.000Z', + 'name': 'Test', + }; + traverseAndConvertDates(testObj, convertUTCToLocal, splitDateTimeLocal); + expect(testObj['createdAt'], isNot(equals('2023-05-01T14:30:00.000Z'))); + expect( + testObj['createdAt'], + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$'), + ); + }); + + test('converts paired fields', () { + final testObj = { + 'startDate': '2023-05-01', + 'startTime': '14:30:00', + 'name': 'Test', + }; + traverseAndConvertDates(testObj, convertUTCToLocal, splitDateTimeLocal); + expect(testObj['startDate'], '2023-05-01'); + expect(testObj['startTime'], matches(r'^\d{2}:\d{2}$')); + }); + + test('converts nested objects', () { + final testObj = { + 'user': { + 'createdAt': '2023-05-01T14:30:00.000Z', + 'name': 'Test User', + }, + }; + traverseAndConvertDates(testObj, convertUTCToLocal, splitDateTimeLocal); + expect( + testObj['user']?['createdAt'], + isNot(equals('2023-05-01T14:30:00.000Z')), + ); + expect( + testObj['user']?['createdAt'], + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$'), + ); + }); + + test('converts objects in lists', () { + withClock(Clock.fixed(DateTime.utc(2023, 5, 1, 12, 0)), () { + final testObj = { + 'items': [ + {'createdAt': '2023-05-01T14:30:00.000Z'}, + {'createdAt': '2023-05-02T15:45:00.000Z'}, + ], + }; + traverseAndConvertDates( + testObj, + convertUTCToLocal, + splitDateTimeLocal, + ); + expect( + testObj['items']?[0]['createdAt'], + isNot(equals('2023-05-01T14:30:00.000Z')), + ); + expect( + testObj['items']?[0]['createdAt'], + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$'), + ); + expect( + testObj['items']?[1]['createdAt'], + isNot(equals('2023-05-02T15:45:00.000Z')), + ); + expect( + testObj['items']?[1]['createdAt'], + matches(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}$'), + ); + }); + }); + }); + }); +}