From 9f188f687da633a98590e57feb26993cf34f1811 Mon Sep 17 00:00:00 2001 From: Xavier Paquet-Rapold <80051842+XavierPaquet-Rapold@users.noreply.github.com> Date: Fri, 5 Jul 2024 21:41:18 -0400 Subject: [PATCH] Feature/move ets api client (#1001) * Import hello api client * Import MonETS api client * Import SignETS api client * Fix imports * Fix tests * Run import_sorter * [BOT] Applying version. * [BOT] Applying format. * Add tests * Fix analyze * [BOT] Applying pod update. * Run CI * Update version * [BOT] Update golden files * [BOT] Update golden files * [BOT] Update golden files --------- Co-authored-by: XavierPaquet-Rapold Co-authored-by: clubapplets-server --- ios/Podfile.lock | 6 - lib/constants/urls.dart | 23 + .../error/not_found/not_found_viewmodel.dart | 2 +- .../app/error/outage/outage_view.dart | 2 +- .../app/error/outage/outage_viewmodel.dart | 6 +- lib/features/app/integration/github_api.dart | 2 +- .../app/integration/launch_url_service.dart | 2 +- .../app/monets_api/models/mon_ets_user.dart | 38 + .../app/monets_api/monets_api_client.dart | 42 + .../app/navigation/navigation_rail.dart | 8 +- .../app/navigation/navigation_service.dart | 2 +- lib/features/app/navigation/router.dart | 31 +- .../app/repository/author_repository.dart | 8 +- .../app/repository/course_repository.dart | 16 +- .../app/repository/news_repository.dart | 8 +- .../app/repository/quick_link_repository.dart | 2 +- .../app/repository/user_repository.dart | 12 +- .../commands/authentificate_command.dart | 33 + .../commands/get_course_reviews_command.dart | 59 ++ .../commands/get_course_summary_command.dart | 72 ++ .../get_courses_activities_command.dart | 98 ++ .../commands/get_courses_command.dart | 37 + .../commands/get_programs_command.dart | 38 + .../get_schedule_activities_command.dart | 65 ++ .../commands/get_sessions_command.dart | 38 + .../commands/get_student_info_command.dart | 34 + .../app/signets-api/models/course.dart | 160 +++ .../signets-api/models/course_activity.dart | 92 ++ .../signets-api/models/course_evaluation.dart | 202 ++++ .../app/signets-api/models/course_review.dart | 104 ++ .../signets-api/models/course_summary.dart | 143 +++ .../signets-api/models/profile_student.dart | 62 ++ .../app/signets-api/models/program.dart | 107 ++ .../signets-api/models/schedule_activity.dart | 134 +++ .../app/signets-api/models/session.dart | 191 ++++ .../signets-api/models/signets_errors.dart | 31 + .../app/signets-api/signets_api_client.dart | 171 ++++ .../app/signets-api/soap_service.dart | 89 ++ lib/features/app/startup/startup_view.dart | 2 +- .../app/startup/startup_viewmodel.dart | 8 +- .../app/widgets/app_widget_service.dart | 2 +- lib/features/app/widgets/base_scaffold.dart | 6 +- lib/features/app/widgets/bottom_bar.dart | 8 +- lib/features/app/widgets/link_web_view.dart | 2 +- lib/features/dashboard/dashboard_view.dart | 24 +- .../dashboard/dashboard_viewmodel.dart | 25 +- .../widgets/course_activity_tile.dart | 4 +- lib/features/ets/ets_view.dart | 6 +- .../commands/get_events_command.dart | 91 ++ .../commands/get_organizer_command.dart | 45 + .../commands/report_news_command.dart | 52 + .../events/api-client/hello_api_client.dart | 76 ++ .../api-client/models/activity_area.dart | 53 + .../ets/events/api-client/models/news.dart | 117 +++ .../events/api-client/models/news_tags.dart | 43 + .../events/api-client/models/organizer.dart | 118 +++ .../api-client/models/paginated_news.dart | 62 ++ .../ets/events/api-client/models/report.dart | 24 + .../events/author/author_info_skeleton.dart | 2 +- .../ets/events/author/author_view.dart | 12 +- .../ets/events/author/author_viewmodel.dart | 3 +- .../news/news-details/news_details_view.dart | 21 +- .../news-details/news_details_viewmodel.dart | 2 +- lib/features/ets/events/news/news_view.dart | 8 +- .../ets/events/news/news_viewmodel.dart | 2 +- .../ets/events/news/widgets/news_card.dart | 6 +- .../ets/events/report-news/report_news.dart | 8 +- .../report-news/report_news_viewmodel.dart | 8 +- .../report-news/report_news_widget.dart | 4 +- .../ets/events/social/social_links_card.dart | 2 +- .../ets/quick-link/quick_links_view.dart | 2 +- .../widgets/security-info/emergency_view.dart | 2 +- .../widgets/security-info/security_view.dart | 4 +- .../security-info/security_viewmodel.dart | 2 +- .../widgets/web_link_card_viewmodel.dart | 4 +- .../more/contributors/contributors_view.dart | 4 +- lib/features/more/faq/faq_view.dart | 2 +- lib/features/more/faq/faq_viewmodel.dart | 2 +- lib/features/more/feedback/feedback_view.dart | 2 +- .../more/feedback/feedback_viewmodel.dart | 5 +- lib/features/more/more_view.dart | 12 +- lib/features/more/more_viewmodel.dart | 16 +- .../more/settings/choose_language_view.dart | 2 +- .../settings/choose_language_viewmodel.dart | 2 +- .../more/settings/settings_manager.dart | 2 +- lib/features/more/settings/settings_view.dart | 2 +- .../calendar_selection_viewmodel.dart | 11 +- .../schedule_default/schedule_default.dart | 2 +- .../schedule_default_view.dart | 2 +- .../schedule_default_viewmodel.dart | 4 +- .../schedule/schedule_settings_viewmodel.dart | 3 +- lib/features/schedule/schedule_view.dart | 14 +- lib/features/schedule/schedule_viewmodel.dart | 15 +- .../schedule/widgets/calendar_selector.dart | 11 +- .../schedule/widgets/schedule_settings.dart | 4 +- .../grade_details/grade_details_view.dart | 9 +- .../grades_details_viewmodel.dart | 9 +- lib/features/student/grades/grades_view.dart | 8 +- .../student/grades/grades_viewmodel.dart | 8 +- .../student/grades/widgets/grade_button.dart | 10 +- .../grades/widgets/grade_evaluation_tile.dart | 8 +- .../student/profile/profile_view.dart | 6 +- .../student/profile/profile_viewmodel.dart | 7 +- lib/features/student/student_view.dart | 8 +- .../student/widgets/student_program.dart | 2 +- .../discovery/discovery_components.dart | 4 +- lib/features/welcome/login/login_view.dart | 10 +- .../welcome/login/login_viewmodel.dart | 2 +- lib/main.dart | 16 +- lib/utils/activity_code.dart | 36 + lib/utils/api_exception.dart | 14 + lib/utils/api_response.dart | 24 + lib/utils/calendar_utils.dart | 7 +- lib/utils/command.dart | 3 + lib/utils/http_exception.dart | 20 + lib/utils/locator.dart | 30 +- pubspec.lock | 53 +- pubspec.yaml | 13 +- test/helpers.dart | 32 +- test/managers/course_repository_test.dart | 20 +- test/managers/quick_link_repository_test.dart | 2 +- test/managers/settings_manager_test.dart | 2 +- test/managers/user_repository_test.dart | 17 +- .../mock/managers/author_repository_mock.dart | 2 +- .../mock/managers/course_repository_mock.dart | 7 +- test/mock/managers/news_repository_mock.dart | 4 +- .../managers/quick_links_repository_mock.dart | 2 +- test/mock/managers/user_repository_mock.dart | 6 +- test/mock/services/github_api_mock.dart | 2 +- test/mock/services/mon_ets_api_mock.dart | 43 + test/mock/services/signets_api_mock.dart | 182 ++++ .../http_client_mock_helper.dart | 45 + test/services/app_widget_service_test.dart | 2 +- test/services/hello_api_client_test.dart | 207 ++++ test/services/monets_api_client_test.dart | 74 ++ test/services/signets_api_client_test.dart | 930 ++++++++++++++++++ test/services/soap_service_test.dart | 32 + test/ui/views/author_view_test.dart | 14 +- test/ui/views/choose_language_view_test.dart | 2 +- test/ui/views/dashboard_view_test.dart | 6 +- test/ui/views/ets_view_test.dart | 20 +- test/ui/views/faq_view_test.dart | 2 +- .../views/goldenFiles/newsDetailsView_1.png | Bin 4342 -> 5228 bytes test/ui/views/grades_details_view_test.dart | 7 +- test/ui/views/grades_view_test.dart | 6 +- test/ui/views/login_view_test.dart | 6 +- test/ui/views/news_details_view_test.dart | 8 +- test/ui/views/news_view_test.dart | 14 +- test/ui/views/profile_view_test.dart | 3 +- test/ui/views/quick_links_view_test.dart | 4 +- test/ui/views/schedule_default_view_test.dart | 6 +- test/ui/views/schedule_view_test.dart | 14 +- test/ui/views/student_view_test.dart | 4 +- test/ui/widgets/base_scaffold_test.dart | 2 +- test/ui/widgets/bottom_bar_test.dart | 4 +- .../ui/widgets/course_activity_tile_test.dart | 2 +- test/ui/widgets/grade_button_test.dart | 5 +- .../widgets/grade_evaluation_tile_test.dart | 4 +- test/ui/widgets/link_web_view_test.dart | 2 +- test/ui/widgets/news_card_test.dart | 4 +- test/ui/widgets/schedule_settings_test.dart | 3 +- test/ui/widgets/student_program_test.dart | 2 +- test/ui/widgets/web_link_card_test.dart | 2 +- test/viewmodels/author_viewmodel_test.dart | 6 +- .../choose_language_viewmodel_test.dart | 4 +- test/viewmodels/dashboard_viewmodel_test.dart | 7 +- test/viewmodels/faq_viewmodel_test.dart | 2 +- test/viewmodels/feedback_viewmodel_test.dart | 4 +- .../grades_details_viewmodel_test.dart | 4 +- test/viewmodels/grades_viewmodel_test.dart | 4 +- test/viewmodels/login_viewmodel_test.dart | 2 +- test/viewmodels/more_viewmodel_test.dart | 10 +- .../news_details_viewmodel_test.dart | 4 +- test/viewmodels/news_viewmodel_test.dart | 7 +- test/viewmodels/not_found_viewmodel_test.dart | 4 +- test/viewmodels/profile_viewmodel_test.dart | 5 +- .../quick_links_viewmodel_test.dart | 2 +- .../schedule_default_viewmodel_test.dart | 2 +- .../schedule_settings_viewmodel_test.dart | 3 +- test/viewmodels/schedule_viewmodel_test.dart | 5 +- test/viewmodels/startup_viewmodel_test.dart | 8 +- .../web_link_card_viewmodel_test.dart | 6 +- 182 files changed, 4863 insertions(+), 437 deletions(-) create mode 100644 lib/features/app/monets_api/models/mon_ets_user.dart create mode 100644 lib/features/app/monets_api/monets_api_client.dart create mode 100644 lib/features/app/signets-api/commands/authentificate_command.dart create mode 100644 lib/features/app/signets-api/commands/get_course_reviews_command.dart create mode 100644 lib/features/app/signets-api/commands/get_course_summary_command.dart create mode 100644 lib/features/app/signets-api/commands/get_courses_activities_command.dart create mode 100644 lib/features/app/signets-api/commands/get_courses_command.dart create mode 100644 lib/features/app/signets-api/commands/get_programs_command.dart create mode 100644 lib/features/app/signets-api/commands/get_schedule_activities_command.dart create mode 100644 lib/features/app/signets-api/commands/get_sessions_command.dart create mode 100644 lib/features/app/signets-api/commands/get_student_info_command.dart create mode 100644 lib/features/app/signets-api/models/course.dart create mode 100644 lib/features/app/signets-api/models/course_activity.dart create mode 100644 lib/features/app/signets-api/models/course_evaluation.dart create mode 100644 lib/features/app/signets-api/models/course_review.dart create mode 100644 lib/features/app/signets-api/models/course_summary.dart create mode 100644 lib/features/app/signets-api/models/profile_student.dart create mode 100644 lib/features/app/signets-api/models/program.dart create mode 100644 lib/features/app/signets-api/models/schedule_activity.dart create mode 100644 lib/features/app/signets-api/models/session.dart create mode 100644 lib/features/app/signets-api/models/signets_errors.dart create mode 100644 lib/features/app/signets-api/signets_api_client.dart create mode 100644 lib/features/app/signets-api/soap_service.dart create mode 100644 lib/features/ets/events/api-client/commands/get_events_command.dart create mode 100644 lib/features/ets/events/api-client/commands/get_organizer_command.dart create mode 100644 lib/features/ets/events/api-client/commands/report_news_command.dart create mode 100644 lib/features/ets/events/api-client/hello_api_client.dart create mode 100644 lib/features/ets/events/api-client/models/activity_area.dart create mode 100644 lib/features/ets/events/api-client/models/news.dart create mode 100644 lib/features/ets/events/api-client/models/news_tags.dart create mode 100644 lib/features/ets/events/api-client/models/organizer.dart create mode 100644 lib/features/ets/events/api-client/models/paginated_news.dart create mode 100644 lib/features/ets/events/api-client/models/report.dart create mode 100644 lib/utils/activity_code.dart create mode 100644 lib/utils/api_exception.dart create mode 100644 lib/utils/api_response.dart create mode 100644 lib/utils/command.dart create mode 100644 lib/utils/http_exception.dart create mode 100644 test/mock/services/mon_ets_api_mock.dart create mode 100644 test/mock/services/signets_api_mock.dart create mode 100644 test/mock/signets-api-client/http_client_mock_helper.dart create mode 100644 test/services/hello_api_client_test.dart create mode 100644 test/services/monets_api_client_test.dart create mode 100644 test/services/signets_api_client_test.dart create mode 100644 test/services/soap_service_test.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 698cd2d00..a375ba373 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -97,8 +97,6 @@ PODS: - Flutter - flutter_custom_tabs_ios (2.0.0): - Flutter - - flutter_keychain (0.0.1): - - Flutter - flutter_secure_storage (6.0.0): - Flutter - fluttertoast (0.0.2): @@ -209,7 +207,6 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_config (from `.symlinks/plugins/flutter_config/ios`) - flutter_custom_tabs_ios (from `.symlinks/plugins/flutter_custom_tabs_ios/ios`) - - flutter_keychain (from `.symlinks/plugins/flutter_keychain/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) @@ -268,8 +265,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_config/ios" flutter_custom_tabs_ios: :path: ".symlinks/plugins/flutter_custom_tabs_ios/ios" - flutter_keychain: - :path: ".symlinks/plugins/flutter_keychain/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" fluttertoast: @@ -326,7 +321,6 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_config: f48f0d47a284f1791aacce2687eabb3309ba7a41 flutter_custom_tabs_ios: 62439c843b2691aae516fd50119a01eb9755fff7 - flutter_keychain: 01aabf894ffe8b01adfda1d9df21c210c1b4b452 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 fluttertoast: 9f2f8e81bb5ce18facb9748d7855bf5a756fe3db google_maps_flutter_ios: c454f18e0e22df6ac0e9f2a4df340858f5a3680c diff --git a/lib/constants/urls.dart b/lib/constants/urls.dart index 0603af335..68850fca1 100644 --- a/lib/constants/urls.dart +++ b/lib/constants/urls.dart @@ -10,4 +10,27 @@ class Urls { static const String clubYoutube = "https://youtube.com/channel/UCiSzzfW1bVbE_0KcEZO52ew"; static const String clubDiscord = "https://discord.gg/adMkWptn6Y"; + + // Urls related to MonETS + static const String monEtsAPI = "https://portail.etsmtl.ca/api/"; + static const String authenticationMonETS = "${monEtsAPI}authentification"; + + /// Urls related to SignetsMobile API + /// For more information about the operations supported see: + /// https://signets-ens.etsmtl.ca/Secure/WebServices/SignetsMobile.asmx + static const String signetsAPI = + "https://signets-ens.etsmtl.ca/Secure/WebServices/SignetsMobile.asmx"; + + // SOAP Operations supported by the Signets API + static const String signetsOperationBase = "http://etsmtl.ca/"; + static const String donneesAuthentificationValides = + "donneesAuthentificationValides"; + static const String infoStudentOperation = "infoEtudiant"; + static const String listProgramsOperation = "listeProgrammes"; + static const String listClassScheduleOperation = "lireHoraireDesSeances"; + static const String listSessionsOperation = "listeSessions"; + static const String listCourseOperation = "listeCours"; + static const String listEvaluationsOperation = "listeElementsEvaluation"; + static const String listeHoraireEtProf = "listeHoraireEtProf"; + static const String readCourseReviewOperation = "lireEvaluationCours"; } diff --git a/lib/features/app/error/not_found/not_found_viewmodel.dart b/lib/features/app/error/not_found/not_found_viewmodel.dart index 31da02cef..f9d04d970 100644 --- a/lib/features/app/error/not_found/not_found_viewmodel.dart +++ b/lib/features/app/error/not_found/not_found_viewmodel.dart @@ -3,9 +3,9 @@ import 'package:rive/rive.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/presentation/rive_animation_service.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/features/app/error/outage/outage_view.dart b/lib/features/app/error/outage/outage_view.dart index 0f500e219..af5230b1f 100644 --- a/lib/features/app/error/outage/outage_view.dart +++ b/lib/features/app/error/outage/outage_view.dart @@ -8,9 +8,9 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/constants/urls.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/app/error/outage/outage_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class OutageView extends StatelessWidget { @override diff --git a/lib/features/app/error/outage/outage_viewmodel.dart b/lib/features/app/error/outage/outage_viewmodel.dart index 27b42310d..d5e108892 100644 --- a/lib/features/app/error/outage/outage_viewmodel.dart +++ b/lib/features/app/error/outage/outage_viewmodel.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/startup/startup_view.dart'; +import 'package:notredame/utils/locator.dart'; class OutageViewModel extends BaseViewModel { int _lastTap = DateTime.now().millisecondsSinceEpoch; diff --git a/lib/features/app/integration/github_api.dart b/lib/features/app/integration/github_api.dart index 5adf6817c..80d9e6c2b 100644 --- a/lib/features/app/integration/github_api.dart +++ b/lib/features/app/integration/github_api.dart @@ -14,9 +14,9 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; // Project imports: -import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/error/internal_info_service.dart'; +import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'package:notredame/utils/locator.dart'; class GithubApi { diff --git a/lib/features/app/integration/launch_url_service.dart b/lib/features/app/integration/launch_url_service.dart index 03dc55624..14c634825 100644 --- a/lib/features/app/integration/launch_url_service.dart +++ b/lib/features/app/integration/launch_url_service.dart @@ -7,8 +7,8 @@ import 'package:url_launcher/url_launcher.dart' as url_launch; // Project imports: import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; class LaunchUrlService { final SettingsManager settingsManager = locator(); diff --git a/lib/features/app/monets_api/models/mon_ets_user.dart b/lib/features/app/monets_api/models/mon_ets_user.dart new file mode 100644 index 000000000..d1d1dd0bf --- /dev/null +++ b/lib/features/app/monets_api/models/mon_ets_user.dart @@ -0,0 +1,38 @@ +/// User received from MonETS after a authentication +class MonETSUser { + static const int studentRoleId = 1; + static const String mainDomain = "ENS"; + + final String domain; + + final int typeUsagerId; + + /// Username of the user + final String username; + + /// Get the universal code extracted from the username + String get universalCode => username.replaceFirst("$domain\\", ""); + + MonETSUser( + {required this.domain, + required this.typeUsagerId, + required this.username}); + + MonETSUser.fromJson(Map json) + : domain = json['Domaine'] as String, + typeUsagerId = json['TypeUsagerId'] as int, + username = json['Username'] as String; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MonETSUser && + runtimeType == other.runtimeType && + domain == other.domain && + typeUsagerId == other.typeUsagerId && + username == other.username; + + @override + int get hashCode => + domain.hashCode ^ typeUsagerId.hashCode ^ username.hashCode; +} diff --git a/lib/features/app/monets_api/monets_api_client.dart b/lib/features/app/monets_api/monets_api_client.dart new file mode 100644 index 000000000..c1e365ca1 --- /dev/null +++ b/lib/features/app/monets_api/monets_api_client.dart @@ -0,0 +1,42 @@ +// Dart imports: +import 'dart:convert'; +import 'dart:io'; + +// Package imports: +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; +import 'package:notredame/utils/http_exception.dart'; + +/// A Wrapper for all calls to MonETS API. +class MonETSAPIClient { + static const String tag = "MonETSApi"; + static const String tagError = "$tag - Error"; + + final http.Client _httpClient; + + MonETSAPIClient({http.Client? client}) + : _httpClient = client ?? IOClient(HttpClient()); + + /// Authenticate the basic MonETS user + /// + /// Throws an [HttpException] if the MonETSApi return anything + /// else than a 200 code + Future authenticate( + {required String username, required String password}) async { + final response = await _httpClient.post( + Uri.parse(Urls.authenticationMonETS), + body: {"Username": username, "Password": password}); + + // Log the http error and throw a exception + if (response.statusCode != 200) { + throw HttpException( + message: response.body, prefix: tagError, code: response.statusCode); + } + return MonETSUser.fromJson( + jsonDecode(response.body) as Map); + } +} diff --git a/lib/features/app/navigation/navigation_rail.dart b/lib/features/app/navigation/navigation_rail.dart index cd4f47c14..83134cd49 100644 --- a/lib/features/app/navigation/navigation_rail.dart +++ b/lib/features/app/navigation/navigation_rail.dart @@ -6,13 +6,13 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; /// Bottom navigation bar for the application. class NavRail extends StatefulWidget { diff --git a/lib/features/app/navigation/navigation_service.dart b/lib/features/app/navigation/navigation_service.dart index 7757080d6..f3b2e75f2 100644 --- a/lib/features/app/navigation/navigation_service.dart +++ b/lib/features/app/navigation/navigation_service.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/utils/locator.dart'; //SERVICE diff --git a/lib/features/app/navigation/router.dart b/lib/features/app/navigation/router.dart index f3b77d52d..a583607b4 100644 --- a/lib/features/app/navigation/router.dart +++ b/lib/features/app/navigation/router.dart @@ -1,36 +1,35 @@ // Flutter imports: import 'package:flutter/material.dart'; -// Package imports: -import 'package:ets_api_clients/models.dart'; - // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/constants/update_code.dart'; -import 'package:notredame/features/ets/events/author/author_view.dart'; +import 'package:notredame/features/app/error/not_found/not_found_view.dart'; +import 'package:notredame/features/app/error/outage/outage_view.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/startup/startup_view.dart'; +import 'package:notredame/features/app/widgets/link_web_view.dart'; +import 'package:notredame/features/dashboard/dashboard_view.dart'; import 'package:notredame/features/ets/ets_view.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/author/author_view.dart'; import 'package:notredame/features/ets/events/news/news-details/news_details_view.dart'; import 'package:notredame/features/ets/events/news/news_view.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; +import 'package:notredame/features/ets/quick-link/quick_links_view.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/security_view.dart'; import 'package:notredame/features/more/about/about_view.dart'; -import 'package:notredame/features/more/settings/choose_language_view.dart'; import 'package:notredame/features/more/contributors/contributors_view.dart'; -import 'package:notredame/features/dashboard/dashboard_view.dart'; import 'package:notredame/features/more/faq/faq_view.dart'; import 'package:notredame/features/more/feedback/feedback_view.dart'; -import 'package:notredame/features/student/grades/grade_details/grade_details_view.dart'; -import 'package:notredame/features/welcome/login/login_view.dart'; import 'package:notredame/features/more/more_view.dart'; -import 'package:notredame/features/app/error/not_found/not_found_view.dart'; -import 'package:notredame/features/app/error/outage/outage_view.dart'; -import 'package:notredame/features/ets/quick-link/quick_links_view.dart'; +import 'package:notredame/features/more/settings/choose_language_view.dart'; +import 'package:notredame/features/more/settings/settings_view.dart'; import 'package:notredame/features/schedule/schedule_default/schedule_default_view.dart'; import 'package:notredame/features/schedule/schedule_view.dart'; -import 'package:notredame/features/ets/quick-link/widgets/security-info/security_view.dart'; -import 'package:notredame/features/more/settings/settings_view.dart'; -import 'package:notredame/features/app/startup/startup_view.dart'; +import 'package:notredame/features/student/grades/grade_details/grade_details_view.dart'; import 'package:notredame/features/student/student_view.dart'; -import 'package:notredame/features/app/widgets/link_web_view.dart'; +import 'package:notredame/features/welcome/login/login_view.dart'; Route generateRoute(RouteSettings routeSettings) { switch (routeSettings.name) { diff --git a/lib/features/app/repository/author_repository.dart b/lib/features/app/repository/author_repository.dart index 4bb3ccc15..6c0f06782 100644 --- a/lib/features/app/repository/author_repository.dart +++ b/lib/features/app/repository/author_repository.dart @@ -1,10 +1,6 @@ -// Flutter imports: -import 'package:ets_api_clients/clients.dart'; - -// Package imports: -import 'package:ets_api_clients/models.dart'; - // Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/utils/locator.dart'; /// Repository to access authors diff --git a/lib/features/app/repository/course_repository.dart b/lib/features/app/repository/course_repository.dart index f944f6de4..71d541db4 100644 --- a/lib/features/app/repository/course_repository.dart +++ b/lib/features/app/repository/course_repository.dart @@ -5,17 +5,23 @@ import 'dart:convert'; import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:logger/logger.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/student/semester_codes.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'package:notredame/utils/cache_exception.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/features/app/repository/news_repository.dart b/lib/features/app/repository/news_repository.dart index c965bcc96..4570baf10 100644 --- a/lib/features/app/repository/news_repository.dart +++ b/lib/features/app/repository/news_repository.dart @@ -1,11 +1,7 @@ -// Flutter imports: -import 'package:ets_api_clients/clients.dart'; - -// Package imports: -import 'package:ets_api_clients/models.dart'; - // Project imports: import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; import 'package:notredame/utils/locator.dart'; /// Repository to access all the news diff --git a/lib/features/app/repository/quick_link_repository.dart b/lib/features/app/repository/quick_link_repository.dart index ca6142571..beab1c7dd 100644 --- a/lib/features/app/repository/quick_link_repository.dart +++ b/lib/features/app/repository/quick_link_repository.dart @@ -5,10 +5,10 @@ import 'dart:convert'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/ets/quick-link/models/quick_links.dart'; import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link_data.dart'; +import 'package:notredame/features/ets/quick-link/models/quick_links.dart'; import 'package:notredame/utils/locator.dart'; class QuickLinkRepository { diff --git a/lib/features/app/repository/user_repository.dart b/lib/features/app/repository/user_repository.dart index 3e5506f3f..73f947a25 100644 --- a/lib/features/app/repository/user_repository.dart +++ b/lib/features/app/repository/user_repository.dart @@ -6,17 +6,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logger/logger.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'package:notredame/utils/cache_exception.dart'; +import 'package:notredame/utils/http_exception.dart'; import 'package:notredame/utils/locator.dart'; class UserRepository { diff --git a/lib/features/app/signets-api/commands/authentificate_command.dart b/lib/features/app/signets-api/commands/authentificate_command.dart new file mode 100644 index 000000000..9a4d12d0e --- /dev/null +++ b/lib/features/app/signets-api/commands/authentificate_command.dart @@ -0,0 +1,33 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Returns whether the user is logged in or not throught the SignetsAPI. +class AuthenticateCommand implements Command { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + + AuthenticateCommand(this.client, this._httpClient, + {required this.username, required this.password}); + + @override + Future execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.donneesAuthentificationValides, username, password) + .buildDocument(); + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.donneesAuthentificationValides); + + /// Build and return the authentication status + return responseBody.innerText == "true"; + } +} diff --git a/lib/features/app/signets-api/commands/get_course_reviews_command.dart b/lib/features/app/signets-api/commands/get_course_reviews_command.dart new file mode 100644 index 000000000..5610d3058 --- /dev/null +++ b/lib/features/app/signets-api/commands/get_course_reviews_command.dart @@ -0,0 +1,59 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the list of all [CourseReview] for the [session] +/// of the student ([username]). +class GetCourseReviewsCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + final Session? session; + + GetCourseReviewsCommand( + this.client, + this._httpClient, { + required this.username, + required this.password, + this.session, + }); + + @override + Future> execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.readCourseReviewOperation, username, password) + .buildDocument(); + + final operationContent = XmlBuilder(); + + operationContent.element("pSession", nest: () { + operationContent.text(session!.shortName); + }); + + body + .findAllElements(Urls.readCourseReviewOperation, + namespace: Urls.signetsOperationBase) + .first + .children + .add(operationContent.buildFragment()); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.readCourseReviewOperation); + + /// Build and return the list of Program + return responseBody + .findAllElements("EvaluationCours") + .map((node) => CourseReview.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_course_summary_command.dart b/lib/features/app/signets-api/commands/get_course_summary_command.dart new file mode 100644 index 000000000..4dd2ca987 --- /dev/null +++ b/lib/features/app/signets-api/commands/get_course_summary_command.dart @@ -0,0 +1,72 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/api_exception.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get all the evaluations (exams) and the summary +/// of [course] for the student ([username]). +class GetCourseSummaryCommand implements Command { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + final Course course; + + GetCourseSummaryCommand( + this.client, + this._httpClient, { + required this.username, + required this.password, + required this.course, + }); + + @override + Future execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listEvaluationsOperation, username, password) + .buildDocument(); + final operationContent = XmlBuilder(); + + // Add the content needed by the operation + operationContent.element("pSigle", nest: () { + operationContent.text(course.acronym); + }); + operationContent.element("pGroupe", nest: () { + operationContent.text(course.group); + }); + operationContent.element("pSession", nest: () { + operationContent.text(course.session); + }); + + body + .findAllElements(Urls.listEvaluationsOperation, + namespace: Urls.signetsOperationBase) + .first + .children + .add(operationContent.buildFragment()); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listEvaluationsOperation); + final errorTag = responseBody.getElement(SignetsError.signetsErrorSoapTag); + if (errorTag != null && + errorTag.innerText.contains(SignetsError.gradesNotAvailable) || + responseBody.findAllElements('ElementEvaluation').isEmpty) { + throw const ApiException( + prefix: SignetsAPIClient.tag, + message: "No grades available", + errorCode: SignetsError.gradesEmpty); + } + + return CourseSummary.fromXmlNode(responseBody); + } +} diff --git a/lib/features/app/signets-api/commands/get_courses_activities_command.dart b/lib/features/app/signets-api/commands/get_courses_activities_command.dart new file mode 100644 index 000000000..ca5a94a9f --- /dev/null +++ b/lib/features/app/signets-api/commands/get_courses_activities_command.dart @@ -0,0 +1,98 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the courses activities for the [session] for +/// the student ([username]). By specifying [courseGroup] we can filter the +/// results to get only the activities for this course. +/// If the [startDate] and/or [endDate] are specified the results will contains +/// all the activities between these dates +class GetCoursesActivitiesCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final RegExp _sessionShortNameRegExp; + final RegExp _courseGroupRegExp; + final String username; + final String password; + final String session; + final String courseGroup; + final DateTime? startDate; + final DateTime? endDate; + + GetCoursesActivitiesCommand( + this.client, + this._httpClient, + this._sessionShortNameRegExp, + this._courseGroupRegExp, { + required this.username, + required this.password, + required this.session, + this.courseGroup = "", + this.startDate, + this.endDate, + }); + + @override + Future> execute() async { + // Validate the format of parameters + if (!_sessionShortNameRegExp.hasMatch(session)) { + throw FormatException("Session $session isn't a correctly formatted"); + } + if (courseGroup.isNotEmpty && !_courseGroupRegExp.hasMatch(courseGroup)) { + throw FormatException( + "CourseGroup $courseGroup isn't a correctly formatted"); + } + if (startDate != null && endDate != null && startDate!.isAfter(endDate!)) { + throw ArgumentError("The startDate can't be after endDate."); + } + + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listClassScheduleOperation, username, password) + .buildDocument(); + final operationContent = XmlBuilder(); + + // Add the content needed by the operation + operationContent.element("pSession", nest: () { + operationContent.text(session); + }); + operationContent.element("pCoursGroupe", nest: () { + operationContent.text(courseGroup); + }); + + operationContent.element("pDateDebut", nest: () { + operationContent.text(startDate == null + ? "" + : "${startDate!.year}-${startDate!.month}-${startDate!.day}"); + }); + operationContent.element("pDateFin", nest: () { + operationContent.text(endDate == null + ? "" + : "${endDate!.year}-${endDate!.month}-${endDate!.day}"); + }); + + // Add the parameters needed inside the request. + body + .findAllElements(Urls.listClassScheduleOperation, + namespace: Urls.signetsOperationBase) + .first + .children + .add(operationContent.buildFragment()); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listClassScheduleOperation); + + /// Build and return the list of CourseActivity + return responseBody + .findAllElements("Seances") + .map((node) => CourseActivity.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_courses_command.dart b/lib/features/app/signets-api/commands/get_courses_command.dart new file mode 100644 index 000000000..c0e6449a1 --- /dev/null +++ b/lib/features/app/signets-api/commands/get_courses_command.dart @@ -0,0 +1,37 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the courses of the student ([username]). +class GetCoursesCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + + GetCoursesCommand(this.client, this._httpClient, + {required this.username, required this.password}); + + @override + Future> execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listCourseOperation, username, password) + .buildDocument(); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listCourseOperation); + + return responseBody + .findAllElements("Cours") + .map((node) => Course.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_programs_command.dart b/lib/features/app/signets-api/commands/get_programs_command.dart new file mode 100644 index 000000000..feacb5c24 --- /dev/null +++ b/lib/features/app/signets-api/commands/get_programs_command.dart @@ -0,0 +1,38 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the list of all the [Program] for the student ([username]). +class GetProgramsCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + + GetProgramsCommand(this.client, this._httpClient, + {required this.username, required this.password}); + + @override + Future> execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listProgramsOperation, username, password) + .buildDocument(); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listProgramsOperation); + + /// Build and return the list of Program + return responseBody + .findAllElements("Programme") + .map((node) => Program.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_schedule_activities_command.dart b/lib/features/app/signets-api/commands/get_schedule_activities_command.dart new file mode 100644 index 000000000..0bba94d35 --- /dev/null +++ b/lib/features/app/signets-api/commands/get_schedule_activities_command.dart @@ -0,0 +1,65 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the courses activities for the [session] for +/// the student ([username]). +class GetScheduleActivitiesCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final RegExp _sessionShortNameRegExp; + final String username; + final String password; + final String session; + + GetScheduleActivitiesCommand( + this.client, + this._httpClient, + this._sessionShortNameRegExp, { + required this.username, + required this.password, + required this.session, + }); + + @override + Future> execute() async { + if (!_sessionShortNameRegExp.hasMatch(session)) { + throw FormatException("Session $session isn't correctly formatted"); + } + + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listeHoraireEtProf, username, password) + .buildDocument(); + final operationContent = XmlBuilder(); + + // Add the content needed by the operation + operationContent.element("pSession", nest: () { + operationContent.text(session); + }); + + // Add the parameters needed inside the request. + body + .findAllElements(Urls.listeHoraireEtProf, + namespace: Urls.signetsOperationBase) + .first + .children + .add(operationContent.buildFragment()); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listeHoraireEtProf); + + /// Build and return the list of CourseActivity + return responseBody + .findAllElements("HoraireActivite") + .map((node) => ScheduleActivity.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_sessions_command.dart b/lib/features/app/signets-api/commands/get_sessions_command.dart new file mode 100644 index 000000000..223be585f --- /dev/null +++ b/lib/features/app/signets-api/commands/get_sessions_command.dart @@ -0,0 +1,38 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the list of all the [Session] for the student ([username]). +class GetSessionsCommand implements Command> { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + + GetSessionsCommand(this.client, this._httpClient, + {required this.username, required this.password}); + + @override + Future> execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.listSessionsOperation, username, password) + .buildDocument(); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.listSessionsOperation); + + /// Build and return the list of Session + return responseBody + .findAllElements("Trimestre") + .map((node) => Session.fromXmlNode(node)) + .toList(); + } +} diff --git a/lib/features/app/signets-api/commands/get_student_info_command.dart b/lib/features/app/signets-api/commands/get_student_info_command.dart new file mode 100644 index 000000000..32e64abdf --- /dev/null +++ b/lib/features/app/signets-api/commands/get_student_info_command.dart @@ -0,0 +1,34 @@ +// Package imports: +import 'package:http/http.dart' as http; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/signets-api/soap_service.dart'; +import 'package:notredame/utils/command.dart'; + +/// Call the SignetsAPI to get the [ProfileStudent] for the student. +class GetStudentInfoCommand implements Command { + final SignetsAPIClient client; + final http.Client _httpClient; + final String username; + final String password; + + GetStudentInfoCommand(this.client, this._httpClient, + {required this.username, required this.password}); + + @override + Future execute() async { + // Generate initial soap envelope + final body = SoapService.buildBasicSOAPBody( + Urls.infoStudentOperation, username, password) + .buildDocument(); + + final responseBody = await SoapService.sendSOAPRequest( + _httpClient, body, Urls.infoStudentOperation); + + // Build and return the info + return ProfileStudent.fromXmlNode(responseBody); + } +} diff --git a/lib/features/app/signets-api/models/course.dart b/lib/features/app/signets-api/models/course.dart new file mode 100644 index 000000000..2a982a17c --- /dev/null +++ b/lib/features/app/signets-api/models/course.dart @@ -0,0 +1,160 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:collection/collection.dart'; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; + +// MODELS + +/// Data-class that represent a course +class Course { + /// Course acronym (ex: LOG430) + final String acronym; + + /// Title of the course (ex: Chimie et matériaux) + final String title; + + /// Course group, on which group the student is registered + final String group; + + /// Session short name during which the course is given (ex: H2020) + final String session; + + /// Code number of the program of which the course is a part of + final String programCode; + + /// Final grade of the course (ex: A+, C, ...) if the course doesn't + /// have a the grade yet the variable will be null. + final String? grade; + + /// Number of credits of the course + final int numberOfCredits; + + /// Current mark, score... of the student for this course. + CourseSummary? summary; + + /// Information about when the course will be evaluated by the student. + List? reviews; + + /// Get the teacher name if available + String? get teacherName => reviews?.first.teacherName; + + /// Determine if we are currently in the review period for this course. + bool get inReviewPeriod { + if (reviews == null) { + return false; + } + + final now = DateTime.now(); + + return now.isAfter(reviews!.first.startAt) && + now.isBefore(reviews!.first.endAt); + } + + /// Determine if all the reviews of this course are completed. + bool? get allReviewsCompleted { + if (reviews == null) { + return true; + } + return reviews!.every((review) => review.isCompleted); + } + + Course( + {required this.acronym, + required this.title, + required this.group, + required this.session, + required this.programCode, + required this.numberOfCredits, + this.grade, + this.summary, + this.reviews}); + + /// Used to create a new [Course] instance from a [XMLElement]. + factory Course.fromXmlNode(XmlElement node) => Course( + acronym: node.getElement('sigle')!.innerText, + title: node.getElement('titreCours')!.innerText, + group: node.getElement('groupe')!.innerText, + session: node.getElement('session')!.innerText, + programCode: node.getElement('programmeEtudes')!.innerText, + numberOfCredits: int.parse(node.getElement('nbCredits')!.innerText), + grade: node.getElement('cote')!.innerText.isEmpty + ? null + : node.getElement('cote')!.innerText); + + /// Used to create [Course] instance from a JSON file + factory Course.fromJson(Map map) => Course( + acronym: map['acronym'] as String, + title: map['title'] as String, + group: map['group'] as String, + session: map['session'] as String, + programCode: map['programCode'] as String, + numberOfCredits: map['numberOfCredits'] as int, + grade: map['grade'] as String?, + summary: map["summary"] != null + ? CourseSummary.fromJson(map["summary"] as Map) + : null, + reviews: map["review"] != null + ? (map["review"] as List) + .map( + (item) => CourseReview.fromJson(item as Map)) + .toList() + : null); + + Map toJson() => { + 'acronym': acronym, + 'title': title, + 'group': group, + 'session': session, + 'programCode': programCode, + 'numberOfCredits': numberOfCredits, + 'grade': grade, + 'summary': summary, + 'reviews': reviews + }; + + @override + String toString() { + return 'Course{' + 'acronym: $acronym, ' + 'title: $title, ' + 'group: $group, ' + 'session: $session, ' + 'programCode: $programCode, ' + 'grade: $grade, ' + 'numberOfCredits: $numberOfCredits, ' + 'summary: $summary, ' + 'reviews: $reviews}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Course && + runtimeType == other.runtimeType && + acronym == other.acronym && + title == other.title && + group == other.group && + session == other.session && + programCode == other.programCode && + grade == other.grade && + numberOfCredits == other.numberOfCredits && + summary == other.summary && + const ListEquality().equals(reviews, other.reviews); + + @override + int get hashCode => + acronym.hashCode ^ + title.hashCode ^ + group.hashCode ^ + session.hashCode ^ + programCode.hashCode ^ + grade.hashCode ^ + numberOfCredits.hashCode ^ + summary.hashCode ^ + reviews.hashCode; +} diff --git a/lib/features/app/signets-api/models/course_activity.dart b/lib/features/app/signets-api/models/course_activity.dart new file mode 100644 index 000000000..1acf6a0ca --- /dev/null +++ b/lib/features/app/signets-api/models/course_activity.dart @@ -0,0 +1,92 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:xml/xml.dart'; + +/// Data-class that represent an activity of a course +class CourseActivity { + /// Course acronym and group + /// Presented like: acronym-group (ex: LOG430-02) + final String courseGroup; + + /// Course name + final String courseName; + + /// Activity name (ex: "Labo A") + final String activityName; + + /// Description of the activity + /// (ex: "Laboratoire (Groupe A)") + final String activityDescription; + + /// Place where the activity is given + final String activityLocation; + + /// Date when the activity start + final DateTime startDateTime; + + /// Date when the activity end + final DateTime endDateTime; + + CourseActivity( + {required this.courseGroup, + required this.courseName, + required this.activityName, + required this.activityDescription, + required this.activityLocation, + required this.startDateTime, + required this.endDateTime}); + + /// Used to create a new [CourseActivity] instance from a [XMLElement]. + factory CourseActivity.fromXmlNode(XmlElement node) => CourseActivity( + courseGroup: node.getElement('coursGroupe')!.innerText, + courseName: node.getElement('libelleCours')!.innerText, + activityName: node.getElement('nomActivite')!.innerText, + activityDescription: node.getElement('descriptionActivite')!.innerText, + activityLocation: node.getElement('local')!.innerText, + startDateTime: DateTime.parse(node.getElement('dateDebut')!.innerText), + endDateTime: DateTime.parse(node.getElement('dateFin')!.innerText)); + + /// Used to create [CourseActivity] instance from a JSON file + factory CourseActivity.fromJson(Map map) => CourseActivity( + courseGroup: map['courseGroup'] as String, + courseName: map['courseName'] as String, + activityName: map['activityName'] as String, + activityDescription: map['activityDescription'] as String, + activityLocation: map['activityLocation'] as String, + startDateTime: DateTime.parse(map['startDateTime'] as String), + endDateTime: DateTime.parse(map['endDateTime'] as String)); + + Map toJson() => { + 'courseGroup': courseGroup, + 'courseName': courseName, + 'activityName': activityName, + 'activityDescription': activityDescription, + 'activityLocation': activityLocation, + 'startDateTime': startDateTime.toString(), + 'endDateTime': endDateTime.toString() + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CourseActivity && + runtimeType == other.runtimeType && + courseGroup == other.courseGroup && + courseName == other.courseName && + activityName == other.activityName && + activityDescription == other.activityDescription && + activityLocation == other.activityLocation && + startDateTime == other.startDateTime && + endDateTime == other.endDateTime; + + @override + int get hashCode => + courseGroup.hashCode ^ + courseName.hashCode ^ + activityName.hashCode ^ + activityDescription.hashCode ^ + activityLocation.hashCode ^ + startDateTime.hashCode ^ + endDateTime.hashCode; +} diff --git a/lib/features/app/signets-api/models/course_evaluation.dart b/lib/features/app/signets-api/models/course_evaluation.dart new file mode 100644 index 000000000..64dee63ac --- /dev/null +++ b/lib/features/app/signets-api/models/course_evaluation.dart @@ -0,0 +1,202 @@ +// Package imports: +import 'package:xml/xml.dart'; + +/// Data-class that represent an evaluation which happened during a course +class CourseEvaluation { + final String courseGroup; + + /// Title of the evaluation (ex: Laboratoire 1) + final String title; + + /// Date on which the evaluation should happen (can also be the date on which + /// the mark was enter in the system) + final DateTime? targetDate; + + /// Mark obtained by the student on the evaluation + /// (ex: 24) + final double? mark; + + /// On how much the evaluation is corrected (ex: 30) + final double correctedEvaluationOutOfFormatted; + + /// On how much the evaluation is corrected included bonus (ex: 30,0+20) + final String correctedEvaluationOutOf; + + /// Weight of the evaluation on the course (ex: 12.5) + final double weight; + + /// Average mark of the students on this evaluation (ex: 30) + final double? passMark; + + /// Standard deviation of the evaluation + final double? standardDeviation; + + /// Median of the evaluation + final double? median; + + /// Percentile rank of the student on this evaluation + final int? percentileRank; + + /// Is the mark of the evaluation published + final bool published; + + /// Message given by the teacher + final String teacherMessage; + + /// Is this evaluation ignored in the final grade + final bool ignore; + + double get markInPercent => mark! / correctedEvaluationOutOfFormatted; + + /// Weighted grade of the evaluation + /// (ex: Mark of 25/50 and 10% weight => 5/10 weighted grade) + double? get weightedGrade => + mark == null || correctedEvaluationOutOfFormatted == 0.0 || weight == 0.0 + ? null + : (mark! / correctedEvaluationOutOfFormatted) * weight; + + CourseEvaluation( + {required this.courseGroup, + required this.title, + required this.weight, + required this.published, + required this.teacherMessage, + required this.ignore, + required this.correctedEvaluationOutOf, + this.mark, + this.passMark, + this.standardDeviation, + this.median, + this.percentileRank, + this.targetDate}) + : correctedEvaluationOutOfFormatted = correctedEvaluationOutOf.isNotEmpty + ? double.parse( + correctedEvaluationOutOf.split("+").first.replaceAll(",", ".")) + : 0.0; + + /// Used to create a new [CourseEvaluation] instance from a [XMLElement]. + factory CourseEvaluation.fromXml(XmlElement node) => CourseEvaluation( + courseGroup: node.getElement('coursGroupe')!.innerText, + title: node.getElement('nom')!.innerText, + mark: node.getElement('note')!.innerText.isNotEmpty + ? double.parse( + node.getElement('note')!.innerText.replaceAll(",", ".")) + : null, + correctedEvaluationOutOf: + node.getElement('corrigeSur')!.innerText.replaceAll(",", "."), + weight: double.parse( + node.getElement('ponderation')!.innerText.replaceAll(",", ".")), + passMark: node.getElement('moyenne')!.innerText.isNotEmpty + ? double.parse( + node.getElement('moyenne')!.innerText.replaceAll(",", ".")) + : null, + standardDeviation: node.getElement('ecartType')!.innerText.isNotEmpty + ? double.parse( + node.getElement('ecartType')!.innerText.replaceAll(",", ".")) + : null, + median: node.getElement('mediane')!.innerText.isNotEmpty + ? double.parse( + node.getElement('mediane')!.innerText.replaceAll(",", ".")) + : null, + percentileRank: node.getElement('rangCentile')!.innerText.isNotEmpty + ? int.parse(node.getElement('rangCentile')!.innerText) + : null, + published: node.getElement('publie')!.innerText == "Oui", + teacherMessage: node.getElement('messageDuProf')!.innerText, + ignore: node.getElement('ignoreDuCalcul')!.innerText == "Oui", + targetDate: node.getElement('dateCible')!.innerText.isNotEmpty + ? DateTime.parse(node.getElement('dateCible')!.innerText) + : null); + + /// Used to create [CourseEvaluation] instance from a JSON file + factory CourseEvaluation.fromJson(Map map) => + CourseEvaluation( + courseGroup: map["courseGroup"] as String, + title: map["title"] as String, + mark: map["mark"] as double?, + correctedEvaluationOutOf: map["correctedEvaluationOutOf"] as String, + weight: map["weight"] as double, + passMark: map["passMark"] as double?, + standardDeviation: map["standardDeviation"] as double?, + median: map["median"] as double?, + percentileRank: map["percentileRank"] as int?, + published: map["published"] as bool, + teacherMessage: map["teacherMessage"] as String, + ignore: map["ignore"] as bool, + targetDate: map["targetDate"] == null + ? null + : DateTime.parse(map["targetDate"] as String)); + + Map toJson() => { + 'courseGroup': courseGroup, + 'title': title, + 'mark': mark, + 'correctedEvaluationOutOf': correctedEvaluationOutOf, + 'weight': weight, + 'passMark': passMark, + 'standardDeviation': standardDeviation, + 'median': median, + 'percentileRank': percentileRank, + 'published': published, + 'teacherMessage': teacherMessage, + 'ignore': ignore, + 'targetDate': targetDate?.toString(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CourseEvaluation && + runtimeType == other.runtimeType && + courseGroup == other.courseGroup && + title == other.title && + targetDate == other.targetDate && + mark == other.mark && + correctedEvaluationOutOf == other.correctedEvaluationOutOf && + correctedEvaluationOutOfFormatted == + other.correctedEvaluationOutOfFormatted && + weight == other.weight && + passMark == other.passMark && + standardDeviation == other.standardDeviation && + median == other.median && + percentileRank == other.percentileRank && + published == other.published && + teacherMessage == other.teacherMessage && + ignore == other.ignore; + + @override + int get hashCode => + courseGroup.hashCode ^ + title.hashCode ^ + targetDate.hashCode ^ + mark.hashCode ^ + correctedEvaluationOutOf.hashCode ^ + correctedEvaluationOutOfFormatted.hashCode ^ + weight.hashCode ^ + passMark.hashCode ^ + standardDeviation.hashCode ^ + median.hashCode ^ + percentileRank.hashCode ^ + published.hashCode ^ + teacherMessage.hashCode ^ + ignore.hashCode; + + @override + String toString() { + return 'Evaluation{' + 'courseGroup: $courseGroup, ' + 'title: $title, ' + 'targetDate: $targetDate, ' + 'mark: $mark, ' + 'correctedEvaluationOutOf: $correctedEvaluationOutOf, ' + 'correctedEvaluationOutOfFormatted: $correctedEvaluationOutOfFormatted, ' + 'weight: $weight, ' + 'passMark: $passMark, ' + 'standardDeviation: $standardDeviation, ' + 'median: $median, ' + 'percentileRank: $percentileRank, ' + 'published: $published, ' + 'teacherMessage: $teacherMessage, ' + 'ignore: $ignore}'; + } +} diff --git a/lib/features/app/signets-api/models/course_review.dart b/lib/features/app/signets-api/models/course_review.dart new file mode 100644 index 000000000..c12be01f2 --- /dev/null +++ b/lib/features/app/signets-api/models/course_review.dart @@ -0,0 +1,104 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:xml/xml.dart'; + +/// Data-class that represent a course evaluation. +class CourseReview { + /// Course acronym (ex: LOG430) + final String acronym; + + /// Course group, on which group the student is registered + final String group; + + /// Name of the professor + final String teacherName; + + /// Date when the evaluation start. + final DateTime startAt; + + /// When the evaluation end. + final DateTime endAt; + + /// Type of the evaluation + final String type; + + /// Is the evaluation completed + final bool isCompleted; + + CourseReview( + {required this.acronym, + required this.group, + required this.teacherName, + required this.type, + required this.startAt, + required this.endAt, + required this.isCompleted}); + + /// Used to create a new [CourseReview] instance from a [XMLElement]. + factory CourseReview.fromXmlNode(XmlElement node) => CourseReview( + acronym: node.getElement('Sigle')!.innerText, + group: node.getElement('Groupe')!.innerText, + teacherName: node.getElement('Enseignant')!.innerText, + type: node.getElement('TypeEvaluation')!.innerText, + startAt: + DateTime.parse(node.getElement('DateDebutEvaluation')!.innerText), + endAt: DateTime.parse(node.getElement('DateFinEvaluation')!.innerText), + isCompleted: + node.getElement('EstComplete')!.innerText.toLowerCase() == 'true'); + + /// Used to create [CourseReview] instance from a JSON file + factory CourseReview.fromJson(Map map) => CourseReview( + acronym: map['acronym'] as String, + group: map['group'] as String, + teacherName: map['teacherName'] as String, + type: map['type'] as String, + startAt: DateTime.parse(map['startAt'] as String), + endAt: DateTime.parse(map['endAt'] as String), + isCompleted: map['isCompleted'] as bool); + + Map toJson() => { + 'acronym': acronym, + 'group': group, + 'teacherName': teacherName, + 'type': type, + 'startAt': startAt.toString(), + 'endAt': endAt.toString(), + 'isCompleted': isCompleted, + }; + + @override + String toString() { + return 'CourseReview{' + 'acronym: $acronym, ' + 'group: $group, ' + 'teacherName: $teacherName, ' + 'type: $type, ' + 'startAt: $startAt, ' + 'endAt: $endAt, ' + 'isCompleted: $isCompleted}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CourseReview && + runtimeType == other.runtimeType && + acronym == other.acronym && + group == other.group && + teacherName == other.teacherName && + startAt == other.startAt && + endAt == other.endAt && + type == other.type && + isCompleted == other.isCompleted; + + @override + int get hashCode => + acronym.hashCode ^ + group.hashCode ^ + teacherName.hashCode ^ + startAt.hashCode ^ + endAt.hashCode ^ + type.hashCode ^ + isCompleted.hashCode; +} diff --git a/lib/features/app/signets-api/models/course_summary.dart b/lib/features/app/signets-api/models/course_summary.dart new file mode 100644 index 000000000..e0281b51e --- /dev/null +++ b/lib/features/app/signets-api/models/course_summary.dart @@ -0,0 +1,143 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:collection/collection.dart'; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; + +/// Data class that represent the current mark, score, ... of a course +class CourseSummary { + /// Mark obtained by the student. + /// (ex: 24) + final double? currentMark; + + /// Mark obtained by the student in percent. + /// (ex: 24) + final double currentMarkInPercent; + + /// On how much the course is actually corrected (ex: 30) + /// Sum of all the evaluations weight already published. + final double markOutOf; + + /// Average mark of the class (ex: 30) + final double? passMark; + + /// Standard deviation of the class + final double? standardDeviation; + + /// Median of the class + final double? median; + + /// Percentile rank of the student on this course + final int? percentileRank; + + /// All the evaluations for this courses. + final List evaluations; + + CourseSummary( + {required this.currentMarkInPercent, + required this.markOutOf, + required this.evaluations, + this.currentMark, + this.passMark, + this.standardDeviation, + this.median, + this.percentileRank}); + + /// Used to create a new [CourseSummary] instance from a [XMLElement]. + factory CourseSummary.fromXmlNode(XmlElement node) => CourseSummary( + currentMark: node.getElement("scoreFinalSur100")!.innerText.isNotEmpty + ? double.parse(node + .getElement("scoreFinalSur100")! + .innerText + .replaceAll(",", ".")) + : null, + currentMarkInPercent: node.getElement("noteACeJour")!.innerText.isNotEmpty + ? double.parse( + node.getElement("noteACeJour")!.innerText.replaceAll(",", ".")) + : 0.0, + markOutOf: node.getElement("tauxPublication")!.innerText.isNotEmpty + ? double.parse(node + .getElement("tauxPublication")! + .innerText + .replaceAll(",", ".")) + : 0.0, + passMark: node.getElement("moyenneClasse")!.innerText.isNotEmpty + ? double.parse( + node.getElement("moyenneClasse")!.innerText.replaceAll(",", ".")) + : null, + standardDeviation: node + .getElement("ecartTypeClasse")! + .innerText + .isNotEmpty + ? double.parse(node.getElement("ecartTypeClasse")!.innerText.replaceAll(",", ".")) + : null, + median: node.getElement("medianeClasse")!.innerText.isNotEmpty ? double.parse(node.getElement("medianeClasse")!.innerText.replaceAll(",", ".")) : null, + percentileRank: node.getElement("rangCentileClasse")!.innerText.isNotEmpty ? int.parse(node.getElement("rangCentileClasse")!.innerText.replaceAll(",0", "")) : null, + evaluations: node.findAllElements("ElementEvaluation").map((node) => CourseEvaluation.fromXml(node)).toList()); + + /// Used to create [CourseSummary] instance from a JSON file + factory CourseSummary.fromJson(Map json) => CourseSummary( + currentMark: json["currentMark"] as double?, + currentMarkInPercent: json["currentMarkInPercent"] as double, + markOutOf: json["markOutOf"] as double, + passMark: json["passMark"] as double?, + standardDeviation: json["standardDeviation"] as double?, + median: json["median"] as double?, + percentileRank: json["percentileRank"] as int?, + evaluations: (json["evaluations"] as List) + .map( + (e) => CourseEvaluation.fromJson(e as Map)) + .toList()); + + Map toJson() => { + "currentMark": currentMark, + "currentMarkInPercent": currentMarkInPercent, + "markOutOf": markOutOf, + "passMark": passMark, + "standardDeviation": standardDeviation, + "median": median, + "percentileRank": percentileRank, + "evaluations": evaluations + }; + + @override + String toString() { + return 'CourseSummary{' + 'currentMark: $currentMark, ' + 'currentMarkInPercent: $currentMarkInPercent, ' + 'markOutOf: $markOutOf, ' + 'passMark: $passMark, ' + 'standardDeviation: $standardDeviation, ' + 'median: $median, ' + 'percentileRank: $percentileRank, ' + 'evaluations: $evaluations}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CourseSummary && + runtimeType == other.runtimeType && + currentMark == other.currentMark && + currentMarkInPercent == other.currentMarkInPercent && + markOutOf == other.markOutOf && + passMark == other.passMark && + standardDeviation == other.standardDeviation && + median == other.median && + percentileRank == other.percentileRank && + const ListEquality().equals(evaluations, other.evaluations); + + @override + int get hashCode => + currentMark.hashCode ^ + currentMarkInPercent.hashCode ^ + markOutOf.hashCode ^ + passMark.hashCode ^ + standardDeviation.hashCode ^ + median.hashCode ^ + percentileRank.hashCode ^ + evaluations.hashCode; +} diff --git a/lib/features/app/signets-api/models/profile_student.dart b/lib/features/app/signets-api/models/profile_student.dart new file mode 100644 index 000000000..21e0c1496 --- /dev/null +++ b/lib/features/app/signets-api/models/profile_student.dart @@ -0,0 +1,62 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:xml/xml.dart'; + +class ProfileStudent { + /// Balance of the student + final String balance; + + /// First name of the student + final String firstName; + + /// Last name of the student + final String lastName; + + /// Permanent code of the student (XXXX00000000) + final String permanentCode; + + ProfileStudent( + {required this.balance, + required this.firstName, + required this.lastName, + required this.permanentCode}); + + /// Used to create a new [ProfileStudent] instance from a [XMLElement]. + factory ProfileStudent.fromXmlNode(XmlElement node) => ProfileStudent( + lastName: node.getElement('nom')!.innerText.trimRight(), + firstName: node.getElement('prenom')!.innerText.trimRight(), + permanentCode: node.getElement('codePerm')!.innerText, + balance: node.getElement('soldeTotal')!.innerText); + + /// Used to create [ProfileStudent] instance from a JSON file + factory ProfileStudent.fromJson(Map map) => ProfileStudent( + lastName: map['nom'] as String, + firstName: map['prenom'] as String, + permanentCode: map['codePerm'] as String, + balance: map['soldeTotal'] as String); + + Map toJson() => { + 'nom': lastName.trimRight(), + 'prenom': firstName.trimRight(), + 'codePerm': permanentCode, + 'soldeTotal': balance + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ProfileStudent && + runtimeType == other.runtimeType && + lastName == other.lastName && + firstName == other.firstName && + permanentCode == other.permanentCode && + balance == other.balance; + + @override + int get hashCode => + lastName.hashCode ^ + firstName.hashCode ^ + permanentCode.hashCode ^ + balance.hashCode; +} diff --git a/lib/features/app/signets-api/models/program.dart b/lib/features/app/signets-api/models/program.dart new file mode 100644 index 000000000..e1cf61ec7 --- /dev/null +++ b/lib/features/app/signets-api/models/program.dart @@ -0,0 +1,107 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:xml/xml.dart'; + +class Program { + /// Name of the program + final String name; + + /// Code of the program (ex: 0725) + final String code; + + /// Average grade of the program (x.xx / 4.30) + final String average; + + /// Number of accumulated credits for the program + final String accumulatedCredits; + + /// Number of registered credits for the program + final String registeredCredits; + + /// Number of completed courses for the program + final String completedCourses; + + /// Number of failed courses for the program + final String failedCourses; + + /// Number of equivalent courses for the program + final String equivalentCourses; + + /// Status of the program (Actif, Diplome) + final String status; + + Program( + {required this.name, + required this.code, + required this.average, + required this.accumulatedCredits, + required this.registeredCredits, + required this.completedCourses, + required this.failedCourses, + required this.equivalentCourses, + required this.status}); + + /// Used to create a new [Program] instance from a [XMLElement]. + factory Program.fromXmlNode(XmlElement node) => Program( + name: node.getElement('libelle')!.innerText, + code: node.getElement('code')!.innerText, + average: node.getElement('moyenne')!.innerText, + accumulatedCredits: node.getElement('nbCreditsCompletes')!.innerText, + registeredCredits: node.getElement('nbCreditsInscrits')!.innerText, + completedCourses: node.getElement('nbCrsReussis')!.innerText, + failedCourses: node.getElement('nbCrsEchoues')!.innerText, + equivalentCourses: node.getElement('nbEquivalences')!.innerText, + status: node.getElement('statut')!.innerText); + + /// Used to create [CourseActivity] instance from a JSON file + factory Program.fromJson(Map map) => Program( + name: map['libelle'] as String, + code: map['code'] as String, + average: map['moyenne'] as String, + accumulatedCredits: map['nbCreditsCompletes'] as String, + registeredCredits: map['nbCreditsInscrits'] as String, + completedCourses: map['nbCrsReussis'] as String, + failedCourses: map['nbCrsEchoues'] as String, + equivalentCourses: map['nbEquivalences'] as String, + status: map['statut'] as String); + + Map toJson() => { + 'libelle': name, + 'code': code, + 'moyenne': average, + 'nbCreditsCompletes': accumulatedCredits, + 'nbCreditsInscrits': registeredCredits, + 'nbCrsReussis': completedCourses, + 'nbCrsEchoues': failedCourses, + 'nbEquivalences': equivalentCourses, + 'statut': status, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Program && + runtimeType == other.runtimeType && + name == other.name && + code == other.code && + average == other.average && + accumulatedCredits == other.accumulatedCredits && + registeredCredits == other.registeredCredits && + completedCourses == other.completedCourses && + failedCourses == other.failedCourses && + equivalentCourses == other.equivalentCourses && + status == other.status; + + @override + int get hashCode => + name.hashCode ^ + code.hashCode ^ + average.hashCode ^ + accumulatedCredits.hashCode ^ + registeredCredits.hashCode ^ + completedCourses.hashCode ^ + failedCourses.hashCode ^ + equivalentCourses.hashCode ^ + status.hashCode; +} diff --git a/lib/features/app/signets-api/models/schedule_activity.dart b/lib/features/app/signets-api/models/schedule_activity.dart new file mode 100644 index 000000000..05aadbcb7 --- /dev/null +++ b/lib/features/app/signets-api/models/schedule_activity.dart @@ -0,0 +1,134 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:intl/intl.dart'; +import 'package:xml/xml.dart'; + +/// Data-class that represent an activity of a course +class ScheduleActivity { + // The course acronym (ex: "ABC123") + final String courseAcronym; + + /// The group number of the activity (ex: "09") + final String courseGroup; + + /// the location of the course + final String courseTitle; + + /// The current day of the week (starting monday) + /// for the ScheduleActivity (ex: 5 for friday) + final int dayOfTheWeek; + + /// The current day of the week (ex: "Vendredi") + final String day; + + /// Date when the activity start (no date part) + final DateTime startTime; + + /// Date when the activity end (no date part) + final DateTime endTime; + + //The code corresponding to the type of schedule activity + final String activityCode; + + /// If the activity schedule is the main activity associated to the course (usually the ) + final bool isPrincipalActivity; + + /// the location of the activity + final String activityLocation; + + /// the name of the activity + final String name; + + ScheduleActivity( + {required this.courseAcronym, + required this.courseGroup, + required this.courseTitle, + required this.dayOfTheWeek, + required this.day, + required this.startTime, + required this.endTime, + required this.activityCode, + required this.isPrincipalActivity, + required this.activityLocation, + required this.name}); + + /// Used to create a new [CourseActivity] instance from a [XMLElement]. + factory ScheduleActivity.fromXmlNode(XmlElement node) => ScheduleActivity( + courseAcronym: node.getElement('sigle')!.innerText, + courseGroup: node.getElement('groupe')!.innerText, + courseTitle: node.getElement('titreCours')!.innerText, + dayOfTheWeek: int.parse(node.getElement('jour')!.innerText), + day: node.getElement('journee')!.innerText, + activityCode: node.getElement('codeActivite')!.innerText, + name: node.getElement('nomActivite')!.innerText, + isPrincipalActivity: + node.getElement('activitePrincipale')!.innerText == "Oui", + startTime: + DateFormat('HH:mm').parse(node.getElement('heureDebut')!.innerText), + endTime: + DateFormat('HH:mm').parse(node.getElement('heureFin')!.innerText), + activityLocation: node.getElement('local')!.innerText, + ); + + /// Used to create [CourseActivity] instance from a JSON file + factory ScheduleActivity.fromJson(Map map) => + ScheduleActivity( + courseAcronym: map['courseAcronym'] as String, + courseGroup: map['courseGroup'] as String, + courseTitle: map['courseTitle'] as String, + dayOfTheWeek: int.parse(map['dayOfTheWeek'] as String), + day: map['day'] as String, + activityCode: map['activityCode'] as String, + name: map['name'] as String, + isPrincipalActivity: + (map['isPrincipalActivity'] as String) == true.toString(), + startTime: DateFormat('HH:mm').parse(map['startTime'] as String), + endTime: DateFormat('HH:mm').parse(map['endTime'] as String), + activityLocation: map['activityLocation'] as String, + ); + + Map toJson() => { + 'courseAcronym': courseAcronym, + 'courseGroup': courseGroup, + 'courseTitle': courseTitle, + 'dayOfTheWeek': dayOfTheWeek.toString(), + 'day': day, + 'activityCode': activityCode, + 'name': name, + 'isPrincipalActivity': isPrincipalActivity.toString(), + 'startTime': DateFormat("HH:mm").format(startTime), + 'endTime': DateFormat("HH:mm").format(endTime), + 'activityLocation': activityLocation + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ScheduleActivity && + courseAcronym == other.courseAcronym && + courseGroup == other.courseGroup && + courseTitle == other.courseTitle && + dayOfTheWeek == other.dayOfTheWeek && + day == other.day && + activityCode == other.activityCode && + name == other.name && + isPrincipalActivity == other.isPrincipalActivity && + startTime == other.startTime && + endTime == other.endTime && + activityLocation == other.activityLocation; + + @override + int get hashCode => + courseAcronym.hashCode ^ + courseGroup.hashCode ^ + courseTitle.hashCode ^ + dayOfTheWeek.hashCode ^ + day.hashCode ^ + activityCode.hashCode ^ + name.hashCode ^ + isPrincipalActivity.hashCode ^ + startTime.hashCode ^ + endTime.hashCode ^ + activityLocation.hashCode; +} diff --git a/lib/features/app/signets-api/models/session.dart b/lib/features/app/signets-api/models/session.dart new file mode 100644 index 000000000..7cdcbddfc --- /dev/null +++ b/lib/features/app/signets-api/models/session.dart @@ -0,0 +1,191 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:xml/xml.dart'; + +/// Data-class that represent a session of courses. +class Session { + /// Short name of the session (like H2020) + final String shortName; + + /// Complete name of the session (like Hiver 2020) + final String name; + + /// Start date of the session, date when the first course is given + final DateTime startDate; + + /// End date of the session + final DateTime endDate; + + /// End date of the courses for this session, date when the last course is given + final DateTime endDateCourses; + + /// Date when the registration for the session start. + final DateTime startDateRegistration; + + /// Date when the registration for the session end. + final DateTime deadlineRegistration; + + /// Date when the cancellation of a course with refund start + final DateTime startDateCancellationWithRefund; + + /// Date when the cancellation of a course with refund end + final DateTime deadlineCancellationWithRefund; + + /// Date when the cancellation of a course with refund end for the new students + final DateTime deadlineCancellationWithRefundNewStudent; + + /// Date when the cancellation of a course without refund start for the new students + final DateTime startDateCancellationWithoutRefundNewStudent; + + /// Date when the cancellation of a course without refund end for the new students + final DateTime deadlineCancellationWithoutRefundNewStudent; + + /// Date when the cancellation of the ASEQ end. + final DateTime deadlineCancellationASEQ; + + Session( + {required this.shortName, + required this.name, + required this.startDate, + required this.endDate, + required this.endDateCourses, + required this.startDateRegistration, + required this.deadlineRegistration, + required this.startDateCancellationWithRefund, + required this.deadlineCancellationWithRefund, + required this.deadlineCancellationWithRefundNewStudent, + required this.startDateCancellationWithoutRefundNewStudent, + required this.deadlineCancellationWithoutRefundNewStudent, + required this.deadlineCancellationASEQ}); + + /// Create a new [Session] instance from a [XMLElement] received from [SignetsApi] + factory Session.fromXmlNode(XmlElement node) => Session( + shortName: node.getElement("abrege")!.innerText, + name: node.getElement("auLong")!.innerText, + startDate: DateTime.parse(node.getElement("dateDebut")!.innerText), + endDate: DateTime.parse(node.getElement("dateFin")!.innerText), + endDateCourses: + DateTime.parse(node.getElement("dateFinCours")!.innerText), + startDateRegistration: + DateTime.parse(node.getElement("dateDebutChemiNot")!.innerText), + deadlineRegistration: + DateTime.parse(node.getElement("dateFinChemiNot")!.innerText), + startDateCancellationWithRefund: DateTime.parse( + node.getElement("dateDebutAnnulationAvecRemboursement")!.innerText), + deadlineCancellationWithRefund: DateTime.parse( + node.getElement("dateFinAnnulationAvecRemboursement")!.innerText), + deadlineCancellationWithRefundNewStudent: DateTime.parse(node + .getElement("dateFinAnnulationAvecRemboursementNouveauxEtudiants")! + .innerText), + startDateCancellationWithoutRefundNewStudent: DateTime.parse(node + .getElement("dateDebutAnnulationSansRemboursementNouveauxEtudiants")! + .innerText), + deadlineCancellationWithoutRefundNewStudent: DateTime.parse(node + .getElement("dateFinAnnulationSansRemboursementNouveauxEtudiants")! + .innerText), + deadlineCancellationASEQ: DateTime.parse( + node.getElement("dateLimitePourAnnulerASEQ")!.innerText)); + + /// Create a new [Session] instance from a JSON file + factory Session.fromJson(Map json) => Session( + shortName: json["shortName"] as String, + name: json["name"] as String, + startDate: DateTime.parse(json["startDate"] as String), + endDate: DateTime.parse(json["endDate"] as String), + endDateCourses: DateTime.parse(json["endDateCourses"] as String), + startDateRegistration: + DateTime.parse(json["startDateRegistration"] as String), + deadlineRegistration: + DateTime.parse(json["deadlineRegistration"] as String), + startDateCancellationWithRefund: + DateTime.parse(json["startDateCancellationWithRefund"] as String), + deadlineCancellationWithRefund: + DateTime.parse(json["deadlineCancellationWithRefund"] as String), + deadlineCancellationWithRefundNewStudent: DateTime.parse( + json["deadlineCancellationWithRefundNewStudent"] as String), + startDateCancellationWithoutRefundNewStudent: DateTime.parse( + json["startDateCancellationWithoutRefundNewStudent"] as String), + deadlineCancellationWithoutRefundNewStudent: DateTime.parse( + json["deadlineCancellationWithoutRefundNewStudent"] as String), + deadlineCancellationASEQ: + DateTime.parse(json["deadlineCancellationASEQ"] as String)); + + Map toJson() => { + 'shortName': shortName, + 'name': name, + 'startDate': startDate.toString(), + 'endDate': endDate.toString(), + 'endDateCourses': endDateCourses.toString(), + 'startDateRegistration': startDateRegistration.toString(), + 'deadlineRegistration': deadlineRegistration.toString(), + 'startDateCancellationWithRefund': + startDateCancellationWithRefund.toString(), + 'deadlineCancellationWithRefund': + deadlineCancellationWithRefund.toString(), + 'deadlineCancellationWithRefundNewStudent': + deadlineCancellationWithRefundNewStudent.toString(), + 'startDateCancellationWithoutRefundNewStudent': + startDateCancellationWithoutRefundNewStudent.toString(), + 'deadlineCancellationWithoutRefundNewStudent': + deadlineCancellationWithoutRefundNewStudent.toString(), + 'deadlineCancellationASEQ': deadlineCancellationASEQ.toString() + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Session && + runtimeType == other.runtimeType && + shortName == other.shortName && + name == other.name && + startDate == other.startDate && + endDate == other.endDate && + endDateCourses == other.endDateCourses && + startDateRegistration == other.startDateRegistration && + deadlineRegistration == other.deadlineRegistration && + startDateCancellationWithRefund == + other.startDateCancellationWithRefund && + deadlineCancellationWithRefund == + other.deadlineCancellationWithRefund && + deadlineCancellationWithRefundNewStudent == + other.deadlineCancellationWithRefundNewStudent && + startDateCancellationWithoutRefundNewStudent == + other.startDateCancellationWithoutRefundNewStudent && + deadlineCancellationWithoutRefundNewStudent == + other.deadlineCancellationWithoutRefundNewStudent && + deadlineCancellationASEQ == other.deadlineCancellationASEQ; + + @override + int get hashCode => + shortName.hashCode ^ + name.hashCode ^ + startDate.hashCode ^ + endDate.hashCode ^ + endDateCourses.hashCode ^ + startDateRegistration.hashCode ^ + deadlineRegistration.hashCode ^ + startDateCancellationWithRefund.hashCode ^ + deadlineCancellationWithRefund.hashCode ^ + deadlineCancellationWithRefundNewStudent.hashCode ^ + startDateCancellationWithoutRefundNewStudent.hashCode ^ + deadlineCancellationWithoutRefundNewStudent.hashCode ^ + deadlineCancellationASEQ.hashCode; + + @override + String toString() { + return 'Session{shortName: $shortName, ' + 'name: $name, ' + 'startDate: $startDate, ' + 'endDate: $endDate, ' + 'endDateCourses: $endDateCourses, ' + 'startDateRegistration: $startDateRegistration, ' + 'deadlineRegistration: $deadlineRegistration, ' + 'startDateCancellationWithRefund: $startDateCancellationWithRefund, ' + 'deadlineCancellationWithRefund: $deadlineCancellationWithRefund, ' + 'deadlineCancellationWithRefundNewStudent: $deadlineCancellationWithRefundNewStudent, ' + 'startDateCancellationWithoutRefundNewStudent: $startDateCancellationWithoutRefundNewStudent, ' + 'deadlineCancellationWithoutRefundNewStudent: $deadlineCancellationWithoutRefundNewStudent, ' + 'deadlineCancellationASEQ: $deadlineCancellationASEQ}'; + } +} diff --git a/lib/features/app/signets-api/models/signets_errors.dart b/lib/features/app/signets-api/models/signets_errors.dart new file mode 100644 index 000000000..b0b2b3734 --- /dev/null +++ b/lib/features/app/signets-api/models/signets_errors.dart @@ -0,0 +1,31 @@ +/// Errors returned by Signets +class SignetsError { + static const String credentialsInvalid = + "Code d'accès ou mot de passe invalide"; + + /// Error message returned when the schedule isn't ready or the session is too + /// old to be saved on the system + static const String scheduleNotAvailable = "Aucune donnée. Cela peut être " + "causé parce que l'horaire détaillé n'est pas encore finalisé (il l'est " + "généralement quelques jours avant le début de la session), ou parce que " + "l'horaire est trop ancien (nous ne gardons que 4 sessions), ou que vous " + "n'êtes pas inscrit, ou que vous n'êtes inscrit qu'à des activité sans " + "horaire, comme un stage ou que que vous êtes en rédaction de mémoire " + "ou de thèse."; + + /// Error message returned when the schedule isn't ready or the session is too + /// old to be saved on the system. Female version. + static const String scheduleNotAvailableF = "Aucune donnée. Cela peut être " + "causé parce que l'horaire détaillé n'est pas encore finalisé (il l'est " + "généralement quelques jours avant le début de la session), ou parce que " + "l'horaire est trop ancien (nous ne gardons que 4 sessions), ou que vous " + "n'êtes pas inscrite, ou que vous n'êtes inscrite qu'à des activité sans " + "horaire, comme un stage ou que que vous êtes en rédaction de mémoire " + "ou de thèse."; + + static const String gradesEmpty = "GRADES_NOT_AVAILABLE"; + static const String gradesNotAvailable = + "Cours ou bordereau de notes inexistant pour"; + + static const String signetsErrorSoapTag = "erreur"; +} diff --git a/lib/features/app/signets-api/signets_api_client.dart b/lib/features/app/signets-api/signets_api_client.dart new file mode 100644 index 000000000..058481f6a --- /dev/null +++ b/lib/features/app/signets-api/signets_api_client.dart @@ -0,0 +1,171 @@ +// Dart imports: +import 'dart:io'; + +// Package imports: +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/commands/authentificate_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_course_reviews_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_course_summary_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_courses_activities_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_courses_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_programs_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_schedule_activities_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_sessions_command.dart'; +import 'package:notredame/features/app/signets-api/commands/get_student_info_command.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; + +/// A Wrapper for all calls to Signets API. +class SignetsAPIClient { + static const String tag = "SignetsApi"; + static const String tagError = "$tag - Error"; + + final http.Client _httpClient; + + SignetsAPIClient({http.Client? client}) + : _httpClient = client ?? IOClient(HttpClient()); + + /// Expression to validate the format of a session short name (ex: A2020) + final RegExp _sessionShortNameRegExp = RegExp("^([A-É-H][0-9]{4})"); + + /// Expression to validate the format of a course (ex: MAT256-01) + final RegExp _courseGroupRegExp = RegExp("^([A-Z]{3}[0-9]{3}-[0-9]{2})"); + + /// Returns whether the user is logged in or not throught the SignetsAPI. + /// Deprecated('This function is deprecated in favor of `MonETSAPIClient.authenticate()`') + Future authenticate( + {required String username, required String password}) { + final command = AuthenticateCommand(this, _httpClient, + username: username, password: password); + return command.execute(); + } + + /// Call the SignetsAPI to get the courses activities for the [session] for + /// the student ([username]). By specifying [courseGroup] we can filter the + /// results to get only the activities for this course. + /// If the [startDate] and/or [endDate] are specified the results will contains + /// all the activities between these dates + Future> getCoursesActivities({ + required String username, + required String password, + String session = "", + String courseGroup = "", + DateTime? startDate, + DateTime? endDate, + }) { + final command = GetCoursesActivitiesCommand( + this, + _httpClient, + _sessionShortNameRegExp, + _courseGroupRegExp, + username: username, + password: password, + session: session, + courseGroup: courseGroup, + startDate: startDate, + endDate: endDate, + ); + return command.execute(); + } + + /// Call the SignetsAPI to get the courses activities for the [session] for + /// the student ([username]). + Future> getScheduleActivities({ + required String username, + required String password, + String session = "", + }) { + final command = GetScheduleActivitiesCommand( + this, + _httpClient, + _sessionShortNameRegExp, + username: username, + password: password, + session: session, + ); + return command.execute(); + } + + /// Call the SignetsAPI to get the courses of the student ([username]). + Future> getCourses({ + required String username, + required String password, + }) { + final command = GetCoursesCommand(this, _httpClient, + username: username, password: password); + return command.execute(); + } + + /// Call the SignetsAPI to get all the evaluations (exams) and the summary + /// of [course] for the student ([username]). + Future getCourseSummary({ + required String username, + required String password, + required Course course, + }) { + final command = GetCourseSummaryCommand( + this, + _httpClient, + username: username, + password: password, + course: course, + ); + return command.execute(); + } + + /// Call the SignetsAPI to get the list of all the [Session] for the student ([username]). + Future> getSessions({ + required String username, + required String password, + }) { + final command = GetSessionsCommand(this, _httpClient, + username: username, password: password); + return command.execute(); + } + + /// Call the SignetsAPI to get the [ProfileStudent] for the student. + Future getStudentInfo({ + required String username, + required String password, + }) { + final command = GetStudentInfoCommand(this, _httpClient, + username: username, password: password); + return command.execute(); + } + + /// Call the SignetsAPI to get the list of all the [Program] for the student ([username]). + Future> getPrograms({ + required String username, + required String password, + }) { + final command = GetProgramsCommand(this, _httpClient, + username: username, password: password); + return command.execute(); + } + + /// Call the SignetsAPI to get the list of all [CourseReview] for the [session] + /// of the student ([username]). + Future> getCourseReviews({ + required String username, + required String password, + Session? session, + }) { + final command = GetCourseReviewsCommand( + this, + _httpClient, + username: username, + password: password, + session: session, + ); + return command.execute(); + } +} diff --git a/lib/features/app/signets-api/soap_service.dart b/lib/features/app/signets-api/soap_service.dart new file mode 100644 index 000000000..3cf1509e8 --- /dev/null +++ b/lib/features/app/signets-api/soap_service.dart @@ -0,0 +1,89 @@ +// Package imports: +import 'package:http/http.dart' as http; +import 'package:xml/xml.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; +import 'package:notredame/utils/api_exception.dart'; + +mixin SoapService { + static const String tag = "SoapService"; + static const String tagError = "$tag - Error"; + + /// Build the default body for communicate with the SignetsAPI. + /// [firstElementName] should be the SOAP operation of the request. + static XmlBuilder buildBasicSOAPBody( + String firstElementName, String username, String password) { + final builder = XmlBuilder(); + + builder.processing('xml', 'version="1.0" encoding="utf-8"'); + builder.element("soap:Envelope", namespaces: { + "http://www.w3.org/2001/XMLSchema-instance": "xsi", + "http://www.w3.org/2001/XMLSchema": "xsd", + "http://schemas.xmlsoap.org/soap/envelope/": "soap" + }, nest: () { + builder.element("soap:Body", nest: () { + // Details of the envelope + builder.element(firstElementName, nest: () { + builder.namespace(Urls.signetsOperationBase); + builder.element("codeAccesUniversel", nest: username); + builder.element("motPasse", nest: password); + }); + }); + }); + + return builder; + } + + /// Build the basic headers for a SOAP request on. + static Map _buildHeaders(String soapAction) => + {"Content-Type": "text/xml", "SOAPAction": soapAction}; + + static String _operationResponseTag(String operation) => "${operation}Result"; + + /// Send a SOAP request to SignetsAPI using [body] as envelope then return + /// the response. + /// Will throw a [ApiException] if an error is returned by the api. + static Future sendSOAPRequest( + http.Client client, XmlDocument body, String operation) async { + // Send the envelope + final response = await client.post(Uri.parse(Urls.signetsAPI), + headers: _buildHeaders(Urls.signetsOperationBase + operation), + body: body.toXmlString()); + + final responseBody = XmlDocument.parse(response.body) + .findAllElements(_operationResponseTag(operation)) + .first; + + // Throw exception if the error tag contains a blocking error + if (responseBody + .findElements(SignetsError.signetsErrorSoapTag) + .isNotEmpty && + responseBody + .findElements(SignetsError.signetsErrorSoapTag) + .first + .innerText + .isNotEmpty) { + switch (responseBody + .findElements(SignetsError.signetsErrorSoapTag) + .first + .innerText) { + case SignetsError.scheduleNotAvailable: + case SignetsError.scheduleNotAvailableF: + // Don't do anything. + break; + case SignetsError.credentialsInvalid: + default: + throw ApiException( + prefix: tagError, + message: responseBody + .findElements(SignetsError.signetsErrorSoapTag) + .first + .innerText); + } + } + + return responseBody; + } +} diff --git a/lib/features/app/startup/startup_view.dart b/lib/features/app/startup/startup_view.dart index cddd2de22..b4a6d4e62 100644 --- a/lib/features/app/startup/startup_view.dart +++ b/lib/features/app/startup/startup_view.dart @@ -6,9 +6,9 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/app/startup/startup_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class StartUpView extends StatelessWidget { @override diff --git a/lib/features/app/startup/startup_viewmodel.dart b/lib/features/app/startup/startup_viewmodel.dart index 33d50bccc..56b559693 100644 --- a/lib/features/app/startup/startup_viewmodel.dart +++ b/lib/features/app/startup/startup_viewmodel.dart @@ -3,16 +3,16 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/constants/update_code.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/error/internal_info_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/app/storage/siren_flutter_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/locator.dart'; class StartUpViewModel extends BaseViewModel { diff --git a/lib/features/app/widgets/app_widget_service.dart b/lib/features/app/widgets/app_widget_service.dart index d5d586340..4856962db 100644 --- a/lib/features/app/widgets/app_widget_service.dart +++ b/lib/features/app/widgets/app_widget_service.dart @@ -6,8 +6,8 @@ import 'package:home_widget/home_widget.dart'; // Project imports: import 'package:notredame/constants/widget_helper.dart'; -import 'package:notredame/features/student/grades/widget_models.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/student/grades/widget_models.dart'; import 'package:notredame/utils/locator.dart'; // MODEL diff --git a/lib/features/app/widgets/base_scaffold.dart b/lib/features/app/widgets/base_scaffold.dart index 43121295d..2ec804c88 100644 --- a/lib/features/app/widgets/base_scaffold.dart +++ b/lib/features/app/widgets/base_scaffold.dart @@ -11,11 +11,11 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/app/navigation/navigation_rail.dart'; -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/widgets/bottom_bar.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; -import 'package:notredame/features/app/widgets/bottom_bar.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; /// Basic Scaffold to avoid boilerplate code in the application. /// Contains a loader controlled by [_isLoading] diff --git a/lib/features/app/widgets/bottom_bar.dart b/lib/features/app/widgets/bottom_bar.dart index a61fd132a..a5a6182ad 100644 --- a/lib/features/app/widgets/bottom_bar.dart +++ b/lib/features/app/widgets/bottom_bar.dart @@ -6,13 +6,13 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; /// Bottom navigation bar for the application. class BottomBar extends StatefulWidget { diff --git a/lib/features/app/widgets/link_web_view.dart b/lib/features/app/widgets/link_web_view.dart index 12d9c6668..e95e2a308 100644 --- a/lib/features/app/widgets/link_web_view.dart +++ b/lib/features/app/widgets/link_web_view.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; // Project imports: -import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; class LinkWebView extends StatefulWidget { final QuickLink _links; diff --git a/lib/features/dashboard/dashboard_view.dart b/lib/features/dashboard/dashboard_view.dart index f74c15104..530edf207 100644 --- a/lib/features/dashboard/dashboard_view.dart +++ b/lib/features/dashboard/dashboard_view.dart @@ -4,32 +4,32 @@ import 'package:flutter/scheduler.dart'; // Package imports: import 'package:auto_size_text/auto_size_text.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/dashboard/progress_bar_text_options.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/constants/update_code.dart'; import 'package:notredame/constants/urls.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; -import 'package:notredame/utils/loading.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; -import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; import 'package:notredame/features/app/widgets/dismissible_card.dart'; -import 'package:notredame/features/student/grades/widgets/grade_button.dart'; +import 'package:notredame/features/dashboard/dashboard_viewmodel.dart'; +import 'package:notredame/features/dashboard/progress_bar_text_options.dart'; +import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; import 'package:notredame/features/dashboard/widgets/haptics_container.dart'; +import 'package:notredame/features/student/grades/widgets/grade_button.dart'; +import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/loading.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; class DashboardView extends StatefulWidget { final UpdateCode updateCode; diff --git a/lib/features/dashboard/dashboard_viewmodel.dart b/lib/features/dashboard/dashboard_viewmodel.dart index 97ed31441..25ad0102b 100644 --- a/lib/features/dashboard/dashboard_viewmodel.dart +++ b/lib/features/dashboard/dashboard_viewmodel.dart @@ -5,32 +5,35 @@ import 'dart:collection'; import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/dashboard/progress_bar_text_options.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/constants/update_code.dart'; import 'package:notredame/constants/widget_helper.dart'; -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/student/grades/widget_models.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/widgets/app_widget_service.dart'; -import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/storage/siren_flutter_service.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/widgets/app_widget_service.dart'; +import 'package:notredame/features/dashboard/progress_bar_text_options.dart'; +import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; +import 'package:notredame/features/student/grades/widget_models.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/activity_code.dart'; +import 'package:notredame/utils/locator.dart'; class DashboardViewModel extends FutureViewModel> { static const String tag = "DashboardViewModel"; diff --git a/lib/features/dashboard/widgets/course_activity_tile.dart b/lib/features/dashboard/widgets/course_activity_tile.dart index 256cd7d5b..7adf736d9 100644 --- a/lib/features/dashboard/widgets/course_activity_tile.dart +++ b/lib/features/dashboard/widgets/course_activity_tile.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:intl/intl.dart'; +// Project imports: +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; + class CourseActivityTile extends StatelessWidget { /// Course to display final CourseActivity activity; diff --git a/lib/features/ets/ets_view.dart b/lib/features/ets/ets_view.dart index 88cc8e888..2b4680e45 100644 --- a/lib/features/ets/ets_view.dart +++ b/lib/features/ets/ets_view.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/utils/locator.dart'; // Project imports: +import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/ets/events/news/news_view.dart'; import 'package:notredame/features/ets/quick-link//quick_links_view.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/utils/locator.dart'; class ETSView extends StatefulWidget { @override diff --git a/lib/features/ets/events/api-client/commands/get_events_command.dart b/lib/features/ets/events/api-client/commands/get_events_command.dart new file mode 100644 index 000000000..33dfa6ab0 --- /dev/null +++ b/lib/features/ets/events/api-client/commands/get_events_command.dart @@ -0,0 +1,91 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:http/http.dart' as http; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; +import 'package:notredame/utils/command.dart'; +import 'package:notredame/utils/http_exception.dart'; + +/// Call the Hello API to get the news +/// [startDate] The start date of the news (optional) +/// [endDate] The end date of the news (optional) +/// [tags] The tags of the news (optional) +/// [activityAreas] The activity areas of the news (optional) +/// [organizerId] The organizer id (optional) +/// [title] The news title (optional) +/// [pageNumber] The page number (default: 1) +/// [pageSize] The page size (default: 10) +class GetEventsCommand implements Command { + final HelloAPIClient client; + final http.Client _httpClient; + final DateTime? startDate; + final DateTime? endDate; + final List? tags; + final List? activityAreas; + final String? organizerId; + final String? title; + final int pageNumber; + final int pageSize; + + GetEventsCommand( + this.client, + this._httpClient, { + this.startDate, + this.endDate, + this.tags, + this.activityAreas, + this.organizerId, + this.title, + this.pageNumber = 1, + this.pageSize = 10, + }); + + @override + Future execute() async { + if (client.apiLink == null || client.apiLink!.isEmpty) { + throw ArgumentError("_apiLink is null or empty"); + } + final query = { + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + }; + + if (startDate != null) { + query['startDate'] = startDate!.toUtc().toIso8601String(); + } + if (endDate != null) { + query['endDate'] = endDate!.toUtc().toIso8601String(); + } + if (tags != null) { + query['tags'] = tags.toString(); + } + if (activityAreas != null) { + query['activityAreas'] = activityAreas.toString(); + } + if (organizerId != null) { + query['organizerId'] = organizerId!; + } + if (title != null) { + query['title'] = title!; + } + + final uri = Uri.https(client.apiLink!, '/api/events'); + final response = await _httpClient.get(uri.replace(queryParameters: query)); + + // Log the http error and throw a exception + if (response.statusCode != 200) { + throw HttpException( + message: response.body, + prefix: HelloAPIClient.tagError, + code: response.statusCode, + ); + } + + return PaginatedNews.fromJson( + jsonDecode(response.body) as Map); + } +} diff --git a/lib/features/ets/events/api-client/commands/get_organizer_command.dart b/lib/features/ets/events/api-client/commands/get_organizer_command.dart new file mode 100644 index 000000000..836ea16fd --- /dev/null +++ b/lib/features/ets/events/api-client/commands/get_organizer_command.dart @@ -0,0 +1,45 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:http/http.dart' as http; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; +import 'package:notredame/utils/api_response.dart'; +import 'package:notredame/utils/command.dart'; +import 'package:notredame/utils/http_exception.dart'; + +/// Call the Hello API to get the organizer +/// [organizerId] The organizer id +class GetOrganizerCommand implements Command { + final HelloAPIClient client; + final http.Client _httpClient; + final String organizerId; + + GetOrganizerCommand(this.client, this._httpClient, this.organizerId); + + @override + Future execute() async { + if (client.apiLink == null || client.apiLink!.isEmpty) { + throw ArgumentError("_apiLink is null or empty"); + } + final uri = Uri.https(client.apiLink!, '/api/organizers/$organizerId'); + final response = await _httpClient.get(uri); + + // Log the http error and throw a exception + if (response.statusCode != 200) { + throw HttpException( + message: response.body, + prefix: HelloAPIClient.tagError, + code: response.statusCode, + ); + } + + final json = jsonDecode(response.body); + return ApiResponse.fromJson( + json as Map, Organizer.fromJson) + .data; + } +} diff --git a/lib/features/ets/events/api-client/commands/report_news_command.dart b/lib/features/ets/events/api-client/commands/report_news_command.dart new file mode 100644 index 000000000..bddc5473f --- /dev/null +++ b/lib/features/ets/events/api-client/commands/report_news_command.dart @@ -0,0 +1,52 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:http/http.dart' as http; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/report.dart'; +import 'package:notredame/utils/command.dart'; +import 'package:notredame/utils/http_exception.dart'; + +/// Call the Hello API to report a news +/// [newsId] The news id +/// [report] The report +class ReportNewsCommand implements Command { + final HelloAPIClient client; + final http.Client _httpClient; + final String newsId; + final Report report; + + ReportNewsCommand(this.client, this._httpClient, this.newsId, this.report); + + @override + Future execute() async { + if (client.apiLink == null || client.apiLink!.isEmpty) { + throw ArgumentError("_apiLink is null or empty"); + } + final uri = Uri.https(client.apiLink!, '/api/reports/$newsId'); + final response = await _httpClient.post( + uri, + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: jsonEncode({ + 'reason': report.reason, + 'category': report.category, + }), + ); + + // Log the http error and throw a exception + if (response.statusCode != 200) { + throw HttpException( + message: response.body, + prefix: HelloAPIClient.tagError, + code: response.statusCode, + ); + } + + return true; + } +} diff --git a/lib/features/ets/events/api-client/hello_api_client.dart b/lib/features/ets/events/api-client/hello_api_client.dart new file mode 100644 index 000000000..f91ee2f6d --- /dev/null +++ b/lib/features/ets/events/api-client/hello_api_client.dart @@ -0,0 +1,76 @@ +// Dart imports: +import 'dart:io'; + +// Package imports: +import 'package:http/http.dart' as http; +import 'package:http/io_client.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/commands/get_events_command.dart'; +import 'package:notredame/features/ets/events/api-client/commands/get_organizer_command.dart'; +import 'package:notredame/features/ets/events/api-client/commands/report_news_command.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; +import 'package:notredame/features/ets/events/api-client/models/report.dart'; + +/// A Wrapper for all calls to Hello API. +class HelloAPIClient { + static const String tag = "HelloApi"; + static const String tagError = "$tag - Error"; + + final http.Client _httpClient; + + String? apiLink; + + HelloAPIClient({http.Client? client}) + : _httpClient = client ?? IOClient(HttpClient()); + + /// Call the Hello API to get the news + /// [startDate] The start date of the news (optional) + /// [endDate] The end date of the news (optional) + /// [tags] The tags of the news (optional) + /// [activityAreas] The activity areas of the news (optional) + /// [organizerId] The organizer id (optional) + /// [title] The news title (optional) + /// [pageNumber] The page number (default: 1) + /// [pageSize] The page size (default: 10) + Future getEvents({ + DateTime? startDate, + DateTime? endDate, + List? tags, + List? activityAreas, + String? organizerId, + String? title, + int pageNumber = 1, + int pageSize = 10, + }) { + final command = GetEventsCommand( + this, + _httpClient, + startDate: startDate, + endDate: endDate, + tags: tags, + activityAreas: activityAreas, + organizerId: organizerId, + title: title, + pageNumber: pageNumber, + pageSize: pageSize, + ); + return command.execute(); + } + + /// Call the Hello API to get the organizer + /// [organizerId] The organizer id + Future getOrganizer(String organizerId) { + final command = GetOrganizerCommand(this, _httpClient, organizerId); + return command.execute(); + } + + /// Call the Hello API to report a news + /// [newsId] The news id + /// [report] The report + Future reportNews(String newsId, Report report) { + final command = ReportNewsCommand(this, _httpClient, newsId, report); + return command.execute(); + } +} diff --git a/lib/features/ets/events/api-client/models/activity_area.dart b/lib/features/ets/events/api-client/models/activity_area.dart new file mode 100644 index 000000000..f2d382cf7 --- /dev/null +++ b/lib/features/ets/events/api-client/models/activity_area.dart @@ -0,0 +1,53 @@ +class ActivityArea { + final String id; + + final String nameFr; + + final String nameEn; + + final DateTime createdAt; + + final DateTime updatedAt; + + ActivityArea( + {required this.id, + required this.nameFr, + required this.nameEn, + required this.createdAt, + required this.updatedAt}); + + /// Used to create [ActivityArea] instance from a JSON file + factory ActivityArea.fromJson(Map map) => ActivityArea( + id: map['id'] as String, + nameFr: map['nameFr'] as String, + nameEn: map['nameEn'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + updatedAt: DateTime.parse(map['updatedAt'] as String)); + + Map toJson() => { + 'id': id, + 'nameFr': nameFr, + 'nameEn': nameEn, + 'createdAt': createdAt.toString(), + 'updatedAt': updatedAt.toString(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ActivityArea && + runtimeType == other.runtimeType && + id == other.id && + nameEn == other.nameEn && + nameFr == other.nameFr && + createdAt == other.createdAt && + updatedAt == other.updatedAt; + + @override + int get hashCode => + id.hashCode ^ + nameFr.hashCode ^ + nameEn.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode; +} diff --git a/lib/features/ets/events/api-client/models/news.dart b/lib/features/ets/events/api-client/models/news.dart new file mode 100644 index 000000000..64e1e0049 --- /dev/null +++ b/lib/features/ets/events/api-client/models/news.dart @@ -0,0 +1,117 @@ +// Project imports: +import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; + +/// Data-class that represent an hello-based news +class News { + /// News unique Id + final String id; + + /// A news title + final String title; + + /// The news content + final String content; + + /// The imageUrl of the news + final String? imageUrl; + + /// The current state of the news + final String state; + + /// The date that the news was created at + final DateTime publicationDate; + + /// Date when the event start + final DateTime eventStartDate; + + /// Date when the event end + final DateTime eventEndDate; + + final DateTime createdAt; + + final DateTime updatedAt; + + final Organizer organizer; + + final List tags; + + News( + {required this.id, + required this.title, + required this.content, + this.imageUrl, + required this.state, + required this.tags, + required this.publicationDate, + required this.eventStartDate, + required this.eventEndDate, + required this.createdAt, + required this.updatedAt, + required this.organizer}); + + /// Used to create [CourseActivity] instance from a JSON file + factory News.fromJson(Map map) => News( + id: map['id'] as String, + title: map['title'] as String, + content: map['content'] as String, + imageUrl: map['imageUrl'] as String?, + state: map['state'] as String, + tags: (map['tags'] as List) + .map((e) => NewsTags.fromJson(e as Map)) + .toList(), + publicationDate: DateTime.parse(map['publicationDate'] as String), + eventStartDate: DateTime.parse(map['eventStartDate'] as String), + eventEndDate: DateTime.parse(map['eventEndDate'] as String), + createdAt: DateTime.parse(map['createdAt'] as String), + updatedAt: DateTime.parse(map['updatedAt'] as String), + organizer: Organizer.fromJson(map['organizer'] as Map)); + + Map toJson() => { + 'id': id, + 'title': title, + 'content': content, + 'imageUrl': imageUrl, + 'state': state, + 'tags': tags.map((e) => e.toJson()).toList(), + 'publicationDate': publicationDate.toString(), + 'eventStartDate': eventStartDate.toString(), + 'eventEndDate': eventEndDate.toString(), + 'createdAt': createdAt.toString(), + 'updatedAt': updatedAt.toString(), + 'organizer': organizer.toJson(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is News && + runtimeType == other.runtimeType && + id == other.id && + title == other.title && + content == other.content && + imageUrl == other.imageUrl && + state == other.state && + tags == other.tags && + publicationDate == other.publicationDate && + eventStartDate == other.eventStartDate && + eventEndDate == other.eventEndDate && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + organizer == other.organizer; + + @override + int get hashCode => + id.hashCode ^ + title.hashCode ^ + content.hashCode ^ + imageUrl.hashCode ^ + state.hashCode ^ + tags.hashCode ^ + publicationDate.hashCode ^ + eventStartDate.hashCode ^ + eventEndDate.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + organizer.hashCode; +} diff --git a/lib/features/ets/events/api-client/models/news_tags.dart b/lib/features/ets/events/api-client/models/news_tags.dart new file mode 100644 index 000000000..4e5cc4f99 --- /dev/null +++ b/lib/features/ets/events/api-client/models/news_tags.dart @@ -0,0 +1,43 @@ +class NewsTags { + final String id; + + final String name; + + final DateTime createdAt; + + final DateTime updatedAt; + + NewsTags( + {required this.id, + required this.name, + required this.createdAt, + required this.updatedAt}); + + /// Used to create [NewsTags] instance from a JSON file + factory NewsTags.fromJson(Map map) => NewsTags( + id: map['id'] as String, + name: map['name'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + updatedAt: DateTime.parse(map['updatedAt'] as String)); + + Map toJson() => { + 'id': id, + 'name': name, + 'createdAt': createdAt.toString(), + 'updatedAt': updatedAt.toString(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is NewsTags && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + createdAt == other.createdAt && + updatedAt == other.updatedAt; + + @override + int get hashCode => + id.hashCode ^ name.hashCode ^ createdAt.hashCode ^ updatedAt.hashCode; +} diff --git a/lib/features/ets/events/api-client/models/organizer.dart b/lib/features/ets/events/api-client/models/organizer.dart new file mode 100644 index 000000000..dec32b5de --- /dev/null +++ b/lib/features/ets/events/api-client/models/organizer.dart @@ -0,0 +1,118 @@ +// Project imports: +import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; + +/// Data-class that represents an organizer +class Organizer { + /// Organizer unique Id + final String id; + + /// Organizer name + final String? name; + + /// Organizer email + final String? email; + + /// Organizer avatar URL + final String? avatarUrl; + + /// Organizer type + final String? type; + + /// Organizer's organization + final String? organization; + + /// Organizer's activity area + final ActivityArea? activityArea; + + /// Whether the organizer is active + final bool? isActive; + + /// Organizer's profile description + final String? profileDescription; + + /// Organizer's Facebook link + final String? facebookLink; + + /// Organizer's Instagram link + final String? instagramLink; + + /// Organizer's TikTok link + final String? tikTokLink; + + /// Organizer's X link + final String? xLink; + + /// Organizer's Discord link + final String? discordLink; + + /// Organizer's LinkedIn link + final String? linkedInLink; + + /// Organizer's Reddit link + final String? redditLink; + + /// Organizer's website link + final String? webSiteLink; + + Organizer({ + required this.id, + this.name, + this.email, + this.avatarUrl, + this.type, + this.organization, + this.activityArea, + this.isActive, + this.profileDescription, + this.facebookLink, + this.instagramLink, + this.tikTokLink, + this.xLink, + this.discordLink, + this.linkedInLink, + this.redditLink, + this.webSiteLink, + }); + + /// Used to create [Organizer] instance from a JSON file + factory Organizer.fromJson(Map map) => Organizer( + id: map['id'] as String, + name: map['name'] as String?, + email: map['email'] as String?, + avatarUrl: map['avatarUrl'] as String?, + type: map['type'] as String?, + organization: map['organization'] as String?, + activityArea: + ActivityArea.fromJson(map['activityArea'] as Map), + isActive: map['isActive'] as bool?, + profileDescription: map['profileDescription'] as String?, + facebookLink: map['facebookLink'] as String?, + instagramLink: map['instagramLink'] as String?, + tikTokLink: map['tikTokLink'] as String?, + xLink: map['xLink'] as String?, + discordLink: map['discordLink'] as String?, + linkedInLink: map['linkedInLink'] as String?, + redditLink: map['redditLink'] as String?, + webSiteLink: map['webSiteLink'] as String?, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'email': email, + 'avatarUrl': avatarUrl, + 'type': type, + 'organization': organization, + 'activityArea': activityArea?.toJson(), + 'isActive': isActive, + 'profileDescription': profileDescription, + 'facebookLink': facebookLink, + 'instagramLink': instagramLink, + 'tikTokLink': tikTokLink, + 'xLink': xLink, + 'discordLink': discordLink, + 'linkedInLink': linkedInLink, + 'redditLink': redditLink, + 'webSiteLink': webSiteLink, + }; +} diff --git a/lib/features/ets/events/api-client/models/paginated_news.dart b/lib/features/ets/events/api-client/models/paginated_news.dart new file mode 100644 index 000000000..2471e60f5 --- /dev/null +++ b/lib/features/ets/events/api-client/models/paginated_news.dart @@ -0,0 +1,62 @@ +// Project imports: +import 'package:notredame/features/ets/events/api-client/models/news.dart'; + +class PaginatedNews { + /// Page Number + final int pageNumber; + + /// Page Size + final int pageSize; + + /// Total Pages + final int totalPages; + + /// Total Records + final int totalRecords; + + /// News + final List news; + + PaginatedNews( + {required this.pageNumber, + required this.pageSize, + required this.totalPages, + required this.totalRecords, + required this.news}); + + factory PaginatedNews.fromJson(Map map) => PaginatedNews( + pageNumber: map['pageNumber'] as int, + pageSize: map['pageSize'] as int, + totalPages: map['totalPages'] as int, + totalRecords: map['totalRecords'] as int, + news: (map['data'] as List) + .map((e) => News.fromJson(e as Map)) + .toList()); + + Map toJson() => { + 'pageNumber': pageNumber, + 'pageSize': pageSize, + 'totalPages': totalPages, + 'totalRecords': totalRecords, + 'data': news, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PaginatedNews && + runtimeType == other.runtimeType && + pageNumber == other.pageNumber && + pageSize == other.pageSize && + totalPages == other.totalPages && + totalRecords == other.totalRecords && + news == other.news; + + @override + int get hashCode => + pageNumber.hashCode ^ + pageSize.hashCode ^ + totalPages.hashCode ^ + totalRecords.hashCode ^ + news.hashCode; +} diff --git a/lib/features/ets/events/api-client/models/report.dart b/lib/features/ets/events/api-client/models/report.dart new file mode 100644 index 000000000..d430b02f3 --- /dev/null +++ b/lib/features/ets/events/api-client/models/report.dart @@ -0,0 +1,24 @@ +/// Data-class that represents a report +class Report { + /// Report reason + final String reason; + + /// Report category + final String category; + + Report({ + required this.reason, + required this.category, + }); + + /// Used to create [Organizer] instance from a JSON file + factory Report.fromJson(Map map) => Report( + reason: map['reason'] as String, + category: map['category'] as String, + ); + + Map toJson() => { + 'reason': reason, + 'category': category, + }; +} diff --git a/lib/features/ets/events/author/author_info_skeleton.dart b/lib/features/ets/events/author/author_info_skeleton.dart index be94b03d0..8bae7a1d2 100644 --- a/lib/features/ets/events/author/author_info_skeleton.dart +++ b/lib/features/ets/events/author/author_info_skeleton.dart @@ -5,8 +5,8 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class AuthorInfoSkeleton extends StatelessWidget { @override diff --git a/lib/features/ets/events/author/author_view.dart b/lib/features/ets/events/author/author_view.dart index 465f2c2fd..cf86d6ca9 100644 --- a/lib/features/ets/events/author/author_view.dart +++ b/lib/features/ets/events/author/author_view.dart @@ -1,23 +1,23 @@ // Flutter imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:notredame/features/ets/events/social/models/social_link.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/author/author_info_skeleton.dart'; +import 'package:notredame/features/ets/events/author/author_viewmodel.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card_skeleton.dart'; +import 'package:notredame/features/ets/events/social/models/social_link.dart'; import 'package:notredame/features/ets/events/social/social_links_card.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class AuthorView extends StatefulWidget { final String authorId; diff --git a/lib/features/ets/events/author/author_viewmodel.dart b/lib/features/ets/events/author/author_viewmodel.dart index 2ce4fae6c..8858a1c42 100644 --- a/lib/features/ets/events/author/author_viewmodel.dart +++ b/lib/features/ets/events/author/author_viewmodel.dart @@ -1,5 +1,4 @@ // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; @@ -8,6 +7,8 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/repository/author_repository.dart'; import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'package:notredame/utils/locator.dart'; class AuthorViewModel extends BaseViewModel implements Initialisable { diff --git a/lib/features/ets/events/news/news-details/news_details_view.dart b/lib/features/ets/events/news/news-details/news_details_view.dart index 2d6931f7e..d2530c8ea 100644 --- a/lib/features/ets/events/news/news-details/news_details_view.dart +++ b/lib/features/ets/events/news/news-details/news_details_view.dart @@ -1,5 +1,4 @@ // Flutter imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -7,23 +6,25 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/ets/events/report-news/report_news_widget.dart'; -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/features/schedule/calendar_selection_viewmodel.dart'; -import 'package:notredame/features/schedule/widgets/calendar_selector.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/news/news-details/news_details_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/ets/events/report-news/report_news_widget.dart'; +import 'package:notredame/features/schedule/calendar_selection_viewmodel.dart'; +import 'package:notredame/features/schedule/widgets/calendar_selector.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; -import 'package:share_plus/share_plus.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; class NewsDetailsView extends StatefulWidget { final News news; diff --git a/lib/features/ets/events/news/news-details/news_details_viewmodel.dart b/lib/features/ets/events/news/news-details/news_details_viewmodel.dart index 2769b6e53..85e0b7f0e 100644 --- a/lib/features/ets/events/news/news-details/news_details_viewmodel.dart +++ b/lib/features/ets/events/news/news-details/news_details_viewmodel.dart @@ -2,10 +2,10 @@ import 'dart:ui'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:stacked/stacked.dart'; // Project imports: +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/utils/app_theme.dart'; class NewsDetailsViewModel extends FutureViewModel { diff --git a/lib/features/ets/events/news/news_view.dart b/lib/features/ets/events/news/news_view.dart index b88c7ed18..839ed5549 100644 --- a/lib/features/ets/events/news/news_view.dart +++ b/lib/features/ets/events/news/news_view.dart @@ -1,18 +1,18 @@ // Flutter imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:stacked/stacked.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; +import 'package:stacked/stacked.dart'; // Project imports: +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/features/ets/events/news/news_viewmodel.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card_skeleton.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class NewsView extends StatefulWidget { @override diff --git a/lib/features/ets/events/news/news_viewmodel.dart b/lib/features/ets/events/news/news_viewmodel.dart index 86a43c3a1..d4e26fb30 100644 --- a/lib/features/ets/events/news/news_viewmodel.dart +++ b/lib/features/ets/events/news/news_viewmodel.dart @@ -1,10 +1,10 @@ // Package imports: import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:stacked/stacked.dart'; -import 'package:ets_api_clients/models.dart'; // Project imports: import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/utils/locator.dart'; class NewsViewModel extends BaseViewModel implements Initialisable { diff --git a/lib/features/ets/events/news/widgets/news_card.dart b/lib/features/ets/events/news/widgets/news_card.dart index a39bd0b6d..677810449 100644 --- a/lib/features/ets/events/news/widgets/news_card.dart +++ b/lib/features/ets/events/news/widgets/news_card.dart @@ -5,13 +5,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:shimmer/shimmer.dart'; import 'package:timeago/timeago.dart' as timeago; -import 'package:ets_api_clients/models.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; class NewsCard extends StatefulWidget { final News news; diff --git a/lib/features/ets/events/report-news/report_news.dart b/lib/features/ets/events/report-news/report_news.dart index bffb91273..84bb59b54 100644 --- a/lib/features/ets/events/report-news/report_news.dart +++ b/lib/features/ets/events/report-news/report_news.dart @@ -1,8 +1,12 @@ -// Project imports: +// Flutter imports: import 'package:flutter/material.dart'; -import 'package:notredame/features/ets/events/report-news/models/report_news.dart'; + +// Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +// Project imports: +import 'package:notredame/features/ets/events/report-news/models/report_news.dart'; + List getLocalizedReportNewsItems(BuildContext context) { return [ ReportNews( diff --git a/lib/features/ets/events/report-news/report_news_viewmodel.dart b/lib/features/ets/events/report-news/report_news_viewmodel.dart index bd1026134..694e4206c 100644 --- a/lib/features/ets/events/report-news/report_news_viewmodel.dart +++ b/lib/features/ets/events/report-news/report_news_viewmodel.dart @@ -1,9 +1,11 @@ // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/models.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:stacked/stacked.dart'; +// Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/report.dart'; +import 'package:notredame/utils/locator.dart'; + class ReportNewsViewModel extends BaseViewModel { final HelloAPIClient _helloApiClient = locator(); diff --git a/lib/features/ets/events/report-news/report_news_widget.dart b/lib/features/ets/events/report-news/report_news_widget.dart index 9a63c7f02..d34c34d60 100644 --- a/lib/features/ets/events/report-news/report_news_widget.dart +++ b/lib/features/ets/events/report-news/report_news_widget.dart @@ -4,13 +4,13 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:notredame/utils/app_theme.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/ets/events/report-news/report_news.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/ets/events/report-news/report_news_viewmodel.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class ReportNews extends StatefulWidget { final bool showHandle; diff --git a/lib/features/ets/events/social/social_links_card.dart b/lib/features/ets/events/social/social_links_card.dart index becefd4b9..9b70057d5 100644 --- a/lib/features/ets/events/social/social_links_card.dart +++ b/lib/features/ets/events/social/social_links_card.dart @@ -7,9 +7,9 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/ets/events/social/models/social_link.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/ets/quick-link/widgets/web_link_card_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class SocialLinks extends StatefulWidget { final bool showHandle; diff --git a/lib/features/ets/quick-link/quick_links_view.dart b/lib/features/ets/quick-link/quick_links_view.dart index 0400af219..3d2c9ee90 100644 --- a/lib/features/ets/quick-link/quick_links_view.dart +++ b/lib/features/ets/quick-link/quick_links_view.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; import 'package:reorderable_grid_view/reorderable_grid_view.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/quick_links_viewmodel.dart'; +import 'package:notredame/features/ets/quick-link/widgets/web_link_card.dart'; import 'package:notredame/utils/app_theme.dart'; class QuickLinksView extends StatefulWidget { diff --git a/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart b/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart index eb4143221..8f10febb6 100644 --- a/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart +++ b/lib/features/ets/quick-link/widgets/security-info/emergency_view.dart @@ -7,8 +7,8 @@ import 'package:webview_flutter/webview_flutter.dart'; // Project imports: import 'package:notredame/features/app/presentation/webview_controller_extension.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class EmergencyView extends StatefulWidget { final String title; diff --git a/lib/features/ets/quick-link/widgets/security-info/security_view.dart b/lib/features/ets/quick-link/widgets/security-info/security_view.dart index 466f050a5..09a41cdd0 100644 --- a/lib/features/ets/quick-link/widgets/security-info/security_view.dart +++ b/lib/features/ets/quick-link/widgets/security-info/security_view.dart @@ -9,10 +9,10 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/emergency_view.dart'; import 'package:notredame/features/ets/quick-link/widgets/security-info/security_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/ets/quick-link/widgets/security-info/emergency_view.dart'; +import 'package:notredame/utils/utils.dart'; class SecurityView extends StatefulWidget { @override diff --git a/lib/features/ets/quick-link/widgets/security-info/security_viewmodel.dart b/lib/features/ets/quick-link/widgets/security-info/security_viewmodel.dart index 346fe0a75..995eea47d 100644 --- a/lib/features/ets/quick-link/widgets/security-info/security_viewmodel.dart +++ b/lib/features/ets/quick-link/widgets/security-info/security_viewmodel.dart @@ -8,9 +8,9 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart'; import 'package:notredame/constants/markers.dart'; import 'package:notredame/features/ets/quick-link/widgets/security-info/models/emergency_procedure.dart'; +import 'package:notredame/features/ets/quick-link/widgets/security-info/models/emergency_procedures.dart'; class SecurityViewModel extends BaseViewModel { GoogleMapController? controller; diff --git a/lib/features/ets/quick-link/widgets/web_link_card_viewmodel.dart b/lib/features/ets/quick-link/widgets/web_link_card_viewmodel.dart index 1b8bff47b..c7fa0a96a 100644 --- a/lib/features/ets/quick-link/widgets/web_link_card_viewmodel.dart +++ b/lib/features/ets/quick-link/widgets/web_link_card_viewmodel.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/utils/locator.dart'; class WebLinkCardViewModel extends BaseViewModel { diff --git a/lib/features/more/contributors/contributors_view.dart b/lib/features/more/contributors/contributors_view.dart index efecdf1ee..f79a2b482 100644 --- a/lib/features/more/contributors/contributors_view.dart +++ b/lib/features/more/contributors/contributors_view.dart @@ -7,10 +7,10 @@ import 'package:github/github.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/contributors/contributors_viewmodel.dart'; import 'package:notredame/utils/loading.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/utils/utils.dart'; class ContributorsView extends StatelessWidget { @override diff --git a/lib/features/more/faq/faq_view.dart b/lib/features/more/faq/faq_view.dart index cdbd740da..0a2525bda 100644 --- a/lib/features/more/faq/faq_view.dart +++ b/lib/features/more/faq/faq_view.dart @@ -7,9 +7,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: +import 'package:notredame/features/more/faq/faq_viewmodel.dart'; import 'package:notredame/features/more/faq/models/faq.dart'; import 'package:notredame/features/more/faq/models/faq_actions.dart'; -import 'package:notredame/features/more/faq/faq_viewmodel.dart'; class FaqView extends StatefulWidget { final Color? backgroundColor; diff --git a/lib/features/more/faq/faq_viewmodel.dart b/lib/features/more/faq/faq_viewmodel.dart index 71e2f7821..424870280 100644 --- a/lib/features/more/faq/faq_viewmodel.dart +++ b/lib/features/more/faq/faq_viewmodel.dart @@ -7,9 +7,9 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/constants/app_info.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/locator.dart'; class FaqViewModel extends BaseViewModel { diff --git a/lib/features/more/feedback/feedback_view.dart b/lib/features/more/feedback/feedback_view.dart index f4298028f..af8748bcb 100644 --- a/lib/features/more/feedback/feedback_view.dart +++ b/lib/features/more/feedback/feedback_view.dart @@ -8,10 +8,10 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/more/feedback/feedback_type.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/more/feedback/feedback_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; +import 'package:notredame/utils/utils.dart'; class FeedbackView extends StatefulWidget { @override diff --git a/lib/features/more/feedback/feedback_viewmodel.dart b/lib/features/more/feedback/feedback_viewmodel.dart index 2f57cf068..731ce2fa6 100644 --- a/lib/features/more/feedback/feedback_viewmodel.dart +++ b/lib/features/more/feedback/feedback_viewmodel.dart @@ -1,4 +1,5 @@ // Dart imports: + // Dart imports: import 'dart:io'; import 'dart:typed_data'; @@ -12,11 +13,11 @@ import 'package:image/image.dart' as image; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/more/feedback/feedback_type.dart'; import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'package:notredame/features/app/integration/github_api.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; +import 'package:notredame/features/more/feedback/feedback_type.dart'; +import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'package:notredame/utils/locator.dart'; class FeedbackViewModel extends FutureViewModel { diff --git a/lib/features/more/more_view.dart b/lib/features/more/more_view.dart index bb5711d5c..520d4189d 100644 --- a/lib/features/more/more_view.dart +++ b/lib/features/more/more_view.dart @@ -9,15 +9,15 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/utils/utils.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/more_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; class MoreView extends StatefulWidget { @override diff --git a/lib/features/more/more_viewmodel.dart b/lib/features/more/more_viewmodel.dart index f78f4d5ee..6937be17a 100644 --- a/lib/features/more/more_viewmodel.dart +++ b/lib/features/more/more_viewmodel.dart @@ -9,20 +9,20 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/integration/launch_url_service.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; -import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/repository/user_repository.dart'; -import 'package:notredame/features/more/feedback/in_app_review_service.dart'; -import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/locator.dart'; class MoreViewModel extends FutureViewModel { /// Cache manager diff --git a/lib/features/more/settings/choose_language_view.dart b/lib/features/more/settings/choose_language_view.dart index 61c640c20..d883ba8a3 100644 --- a/lib/features/more/settings/choose_language_view.dart +++ b/lib/features/more/settings/choose_language_view.dart @@ -6,9 +6,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/more/settings/choose_language_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class ChooseLanguageView extends StatefulWidget { @override diff --git a/lib/features/more/settings/choose_language_viewmodel.dart b/lib/features/more/settings/choose_language_viewmodel.dart index c0ed74e97..644bc4dd5 100644 --- a/lib/features/more/settings/choose_language_viewmodel.dart +++ b/lib/features/more/settings/choose_language_viewmodel.dart @@ -3,9 +3,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/utils/locator.dart'; class ChooseLanguageViewModel extends BaseViewModel { diff --git a/lib/features/more/settings/settings_manager.dart b/lib/features/more/settings/settings_manager.dart index e8dc53f60..9cd1fb68c 100644 --- a/lib/features/more/settings/settings_manager.dart +++ b/lib/features/more/settings/settings_manager.dart @@ -12,8 +12,8 @@ import 'package:table_calendar/table_calendar.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/utils/locator.dart'; class SettingsManager with ChangeNotifier { diff --git a/lib/features/more/settings/settings_view.dart b/lib/features/more/settings/settings_view.dart index bef0ef57c..e5a178f39 100644 --- a/lib/features/more/settings/settings_view.dart +++ b/lib/features/more/settings/settings_view.dart @@ -6,9 +6,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: +import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/more/settings/settings_viewmodel.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; class SettingsView extends StatefulWidget { @override diff --git a/lib/features/schedule/calendar_selection_viewmodel.dart b/lib/features/schedule/calendar_selection_viewmodel.dart index 864d9f45e..38ce31980 100644 --- a/lib/features/schedule/calendar_selection_viewmodel.dart +++ b/lib/features/schedule/calendar_selection_viewmodel.dart @@ -1,12 +1,17 @@ +// Dart imports: import 'dart:collection'; -import 'package:device_calendar/device_calendar.dart'; -import 'package:ets_api_clients/models.dart'; +// Flutter imports: import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; + +// Package imports: +import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +// Project imports: import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; import 'package:notredame/utils/calendar_utils.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/features/schedule/schedule_default/schedule_default.dart b/lib/features/schedule/schedule_default/schedule_default.dart index ef7243dae..a3524762e 100644 --- a/lib/features/schedule/schedule_default/schedule_default.dart +++ b/lib/features/schedule/schedule_default/schedule_default.dart @@ -6,8 +6,8 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/features/schedule/widgets/schedule_calendar_tile.dart'; +import 'package:notredame/utils/app_theme.dart'; class ScheduleDefault extends StatefulWidget { final List> calendarEvents; diff --git a/lib/features/schedule/schedule_default/schedule_default_view.dart b/lib/features/schedule/schedule_default/schedule_default_view.dart index 99b03adfa..75e1d6119 100644 --- a/lib/features/schedule/schedule_default/schedule_default_view.dart +++ b/lib/features/schedule/schedule_default/schedule_default_view.dart @@ -6,9 +6,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/schedule/schedule_default/schedule_default_viewmodel.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/schedule/schedule_default/schedule_default.dart'; +import 'package:notredame/features/schedule/schedule_default/schedule_default_viewmodel.dart'; class ScheduleDefaultView extends StatefulWidget { final String? sessionCode; diff --git a/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart b/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart index 294a9a83a..27c8cffcc 100644 --- a/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart +++ b/lib/features/schedule/schedule_default/schedule_default_viewmodel.dart @@ -3,13 +3,13 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:calendar_view/calendar_view.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; class ScheduleDefaultViewModel extends FutureViewModel>> { diff --git a/lib/features/schedule/schedule_settings_viewmodel.dart b/lib/features/schedule/schedule_settings_viewmodel.dart index 74658d188..5074d5087 100644 --- a/lib/features/schedule/schedule_settings_viewmodel.dart +++ b/lib/features/schedule/schedule_settings_viewmodel.dart @@ -2,14 +2,15 @@ import 'package:calendar_view/calendar_view.dart'; import 'package:collection/collection.dart'; import 'package:enum_to_string/enum_to_string.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:stacked/stacked.dart'; import 'package:table_calendar/table_calendar.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; +import 'package:notredame/utils/activity_code.dart'; import 'package:notredame/utils/locator.dart'; class ScheduleSettingsViewModel diff --git a/lib/features/schedule/schedule_view.dart b/lib/features/schedule/schedule_view.dart index a7eb7e74f..f4d64e461 100644 --- a/lib/features/schedule/schedule_view.dart +++ b/lib/features/schedule/schedule_view.dart @@ -5,26 +5,26 @@ import 'package:flutter/services.dart'; // Package imports: import 'package:calendar_view/calendar_view.dart' as calendar_view; -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; -import 'package:notredame/features/schedule/widgets/calendar_selector.dart'; import 'package:stacked/stacked.dart'; import 'package:table_calendar/table_calendar.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/schedule/schedule_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; +import 'package:notredame/features/schedule/schedule_viewmodel.dart'; +import 'package:notredame/features/schedule/widgets/calendar_selector.dart'; import 'package:notredame/features/schedule/widgets/schedule_calendar_tile.dart'; import 'package:notredame/features/schedule/widgets/schedule_settings.dart'; +import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; class ScheduleView extends StatefulWidget { @visibleForTesting diff --git a/lib/features/schedule/schedule_viewmodel.dart b/lib/features/schedule/schedule_viewmodel.dart index 441931f81..1377ca3e5 100644 --- a/lib/features/schedule/schedule_viewmodel.dart +++ b/lib/features/schedule/schedule_viewmodel.dart @@ -1,11 +1,10 @@ // Flutter imports: import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; // Package imports: import 'package:calendar_view/calendar_view.dart'; +import 'package:collection/collection.dart'; import 'package:enum_to_string/enum_to_string.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -13,14 +12,18 @@ import 'package:stacked/stacked.dart'; import 'package:table_calendar/table_calendar.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/activity_code.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; class ScheduleViewModel extends FutureViewModel> { /// Load the events diff --git a/lib/features/schedule/widgets/calendar_selector.dart b/lib/features/schedule/widgets/calendar_selector.dart index 96470af78..6a4e28d1e 100644 --- a/lib/features/schedule/widgets/calendar_selector.dart +++ b/lib/features/schedule/widgets/calendar_selector.dart @@ -1,12 +1,17 @@ +// Dart imports: import 'dart:collection'; -import 'package:device_calendar/device_calendar.dart'; +// Flutter imports: import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; + +// Package imports: +import 'package:device_calendar/device_calendar.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:fluttertoast/fluttertoast.dart'; -import 'package:notredame/utils/app_theme.dart'; +// Project imports: import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/calendar_utils.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/features/schedule/widgets/schedule_settings.dart b/lib/features/schedule/widgets/schedule_settings.dart index 9326518d1..a9127e423 100644 --- a/lib/features/schedule/widgets/schedule_settings.dart +++ b/lib/features/schedule/widgets/schedule_settings.dart @@ -3,15 +3,15 @@ import 'package:flutter/material.dart'; // Package imports: import 'package:calendar_view/calendar_view.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; import 'package:table_calendar/table_calendar.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/schedule/schedule_settings_viewmodel.dart'; +import 'package:notredame/utils/activity_code.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class ScheduleSettings extends StatefulWidget { final bool showHandle; diff --git a/lib/features/student/grades/grade_details/grade_details_view.dart b/lib/features/student/grades/grade_details/grade_details_view.dart index 226b9561c..37ed4d83a 100644 --- a/lib/features/student/grades/grade_details/grade_details_view.dart +++ b/lib/features/student/grades/grade_details/grade_details_view.dart @@ -3,18 +3,19 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/utils/utils.dart'; -import 'package:notredame/features/student/grades/grade_details/grades_details_viewmodel.dart'; -import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/student/grades/grade_details/grades_details_viewmodel.dart'; import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; import 'package:notredame/features/student/grades/widgets/grade_evaluation_tile.dart'; import 'package:notredame/features/student/grades/widgets/grade_not_available.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/utils.dart'; class GradesDetailsView extends StatefulWidget { final Course course; diff --git a/lib/features/student/grades/grade_details/grades_details_viewmodel.dart b/lib/features/student/grades/grade_details/grades_details_viewmodel.dart index c7d12ec08..3f1439a5f 100644 --- a/lib/features/student/grades/grade_details/grades_details_viewmodel.dart +++ b/lib/features/student/grades/grade_details/grades_details_viewmodel.dart @@ -2,20 +2,21 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/api_exception.dart'; +import 'package:notredame/utils/locator.dart'; class GradesDetailsViewModel extends FutureViewModel { /// Used to get the courses of the student diff --git a/lib/features/student/grades/grades_view.dart b/lib/features/student/grades/grades_view.dart index 7d30ba83d..91b7cba1d 100644 --- a/lib/features/student/grades/grades_view.dart +++ b/lib/features/student/grades/grades_view.dart @@ -3,20 +3,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/student/grades/grades_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/student/grades/widgets/grade_button.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; -import 'package:notredame/features/student/grades/widgets/grade_button.dart'; +import 'package:notredame/utils/locator.dart'; class GradesView extends StatefulWidget { @override diff --git a/lib/features/student/grades/grades_viewmodel.dart b/lib/features/student/grades/grades_viewmodel.dart index f1dee4420..df30d25c9 100644 --- a/lib/features/student/grades/grades_viewmodel.dart +++ b/lib/features/student/grades/grades_viewmodel.dart @@ -2,20 +2,20 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:notredame/features/student/semester_codes.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/student/semester_codes.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/locator.dart'; class GradesViewModel extends FutureViewModel>> { /// Used to get the courses of the student diff --git a/lib/features/student/grades/widgets/grade_button.dart b/lib/features/student/grades/widgets/grade_button.dart index c0cb8770e..01daf72e4 100644 --- a/lib/features/student/grades/widgets/grade_button.dart +++ b/lib/features/student/grades/widgets/grade_button.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; class GradeButton extends StatelessWidget { final Course course; diff --git a/lib/features/student/grades/widgets/grade_evaluation_tile.dart b/lib/features/student/grades/widgets/grade_evaluation_tile.dart index 26062feb9..bbeed31a6 100644 --- a/lib/features/student/grades/widgets/grade_evaluation_tile.dart +++ b/lib/features/student/grades/widgets/grade_evaluation_tile.dart @@ -5,17 +5,17 @@ import 'dart:math'; import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:intl/intl.dart'; // Project imports: +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; +import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; +import 'package:notredame/features/welcome/discovery/discovery_components.dart'; import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; -import 'package:notredame/features/student/grades/widgets/grade_circular_progress.dart'; +import 'package:notredame/utils/utils.dart'; class GradeEvaluationTile extends StatefulWidget { final bool completed; diff --git a/lib/features/student/profile/profile_view.dart b/lib/features/student/profile/profile_view.dart index bb3f96206..980ee632f 100644 --- a/lib/features/student/profile/profile_view.dart +++ b/lib/features/student/profile/profile_view.dart @@ -3,18 +3,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:percent_indicator/percent_indicator.dart'; import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/features/student/profile/profile_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/student/widgets/student_program.dart'; import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/utils/loading.dart'; -import 'package:notredame/features/student/widgets/student_program.dart'; +import 'package:notredame/utils/locator.dart'; class ProfileView extends StatefulWidget { @override diff --git a/lib/features/student/profile/profile_viewmodel.dart b/lib/features/student/profile/profile_viewmodel.dart index be5ded22b..53f997a33 100644 --- a/lib/features/student/profile/profile_viewmodel.dart +++ b/lib/features/student/profile/profile_viewmodel.dart @@ -1,13 +1,14 @@ // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/student/profile/programs_credits.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/student/profile/programs_credits.dart'; import 'package:notredame/utils/locator.dart'; class ProfileViewModel extends FutureViewModel> { diff --git a/lib/features/student/student_view.dart b/lib/features/student/student_view.dart index e9bc7c927..5e250d657 100644 --- a/lib/features/student/student_view.dart +++ b/lib/features/student/student_view.dart @@ -6,12 +6,12 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/app/widgets/base_scaffold.dart'; import 'package:notredame/features/student/grades/grades_view.dart'; import 'package:notredame/features/student/profile/profile_view.dart'; -import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/welcome/discovery/discovery_components.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; +import 'package:notredame/utils/app_theme.dart'; class StudentView extends StatefulWidget { @override diff --git a/lib/features/student/widgets/student_program.dart b/lib/features/student/widgets/student_program.dart index f02d6a50e..cfd3b8123 100644 --- a/lib/features/student/widgets/student_program.dart +++ b/lib/features/student/widgets/student_program.dart @@ -5,10 +5,10 @@ import 'dart:math'; import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: +import 'package:notredame/features/app/signets-api/models/program.dart'; import 'package:notredame/utils/app_theme.dart'; class StudentProgram extends StatefulWidget { diff --git a/lib/features/welcome/discovery/discovery_components.dart b/lib/features/welcome/discovery/discovery_components.dart index 7d10a096d..e74ed8365 100644 --- a/lib/features/welcome/discovery/discovery_components.dart +++ b/lib/features/welcome/discovery/discovery_components.dart @@ -6,14 +6,14 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // Project imports: -import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/welcome/discovery/models/discovery.dart'; +import 'package:notredame/features/welcome/discovery/models/discovery_ids.dart'; import 'package:notredame/features/welcome/discovery/models/group_discovery.dart'; -import 'package:notredame/utils/locator.dart'; import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; List discoveryComponents(BuildContext context) { return [ diff --git a/lib/features/welcome/login/login_view.dart b/lib/features/welcome/login/login_view.dart index a71d7618b..ac30e3e51 100644 --- a/lib/features/welcome/login/login_view.dart +++ b/lib/features/welcome/login/login_view.dart @@ -8,16 +8,16 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:stacked/stacked.dart'; // Project imports: -import 'package:notredame/features/app/navigation/router_paths.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/welcome/login/login_mask.dart'; -import 'package:notredame/utils/utils.dart'; import 'package:notredame/features/welcome/login/login_viewmodel.dart'; -import 'package:notredame/utils/locator.dart'; -import 'package:notredame/utils/app_theme.dart'; import 'package:notredame/features/welcome/widgets/password_text_field.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; +import 'package:notredame/utils/utils.dart'; class LoginView extends StatefulWidget { @override diff --git a/lib/features/welcome/login/login_viewmodel.dart b/lib/features/welcome/login/login_viewmodel.dart index 8c798ee9f..bb2f0f3a6 100644 --- a/lib/features/welcome/login/login_viewmodel.dart +++ b/lib/features/welcome/login/login_viewmodel.dart @@ -5,9 +5,9 @@ import 'package:stacked/stacked.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router_paths.dart'; import 'package:notredame/features/app/repository/user_repository.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/utils/locator.dart'; diff --git a/lib/main.dart b/lib/main.dart index c40861a95..de576f902 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; // Flutter imports: -import 'package:ets_api_clients/clients.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -18,19 +17,20 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; // Project imports: -import 'package:notredame/features/more/feedback/models/custom_feedback_localization.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/widgets/app_widget_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/error/outage/outage_view.dart'; import 'package:notredame/features/app/integration/firebase_options.dart'; -import 'package:notredame/utils/locator.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/navigation/router.dart'; -import 'package:notredame/utils/app_theme.dart'; -import 'package:notredame/features/app/error/outage/outage_view.dart'; import 'package:notredame/features/app/startup/startup_view.dart'; +import 'package:notredame/features/app/widgets/app_widget_service.dart'; +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/more/feedback/models/custom_feedback_localization.dart'; import 'package:notredame/features/more/feedback/widgets/custom_feedback.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; +import 'package:notredame/utils/app_theme.dart'; +import 'package:notredame/utils/locator.dart'; Future main() async { setupLocator(); diff --git a/lib/utils/activity_code.dart b/lib/utils/activity_code.dart new file mode 100644 index 000000000..e8da3516b --- /dev/null +++ b/lib/utils/activity_code.dart @@ -0,0 +1,36 @@ +/// A class containing all the ActivityCode for a schedule activity +/// +/// Actual known definition of the code id of a schedule activity: +/// A -> Atelier [workshop] +/// B -> ??? +/// C -> Activité de cours [lectureCourse] +/// D -> ??? +/// E -> Travaux pratiques [practicalWork] +/// F -> Travaux pratiques aux 2 semaines [practicalWorkEvery2Weeks] +/// G -> ??? +/// H -> ??? +/// I -> ??? +/// J -> ??? (Could be "Travaux pratiques (Groupe A)") +/// K -> ??? (Could be "Travaux pratiques (Groupe B)") +/// L -> Laboratoire [lab] +/// M -> Laboratoire aux 2 semaines [labEvery2Weeks] +/// N -> ??? +/// O -> ??? +/// P -> ??? +/// Q -> Laboratoire (Groupe A) [labGroupA] +/// R -> Laboratoire (Groupe B) [labGroupB] +class ActivityCode { + static const String practicalWork = "E"; + static const String practicalWorkEvery2Weeks = "F"; + static const String workshop = "A"; + static const String lectureCourse = "C"; + static const String lab = "L"; + static const String labEvery2Weeks = "M"; + static const String labGroupA = "Q"; + static const String labGroupB = "R"; +} + +class ActivityDescriptionName { + static const String labA = "Laboratoire (Groupe A)"; + static const String labB = "Laboratoire (Groupe B)"; +} diff --git a/lib/utils/api_exception.dart b/lib/utils/api_exception.dart new file mode 100644 index 000000000..e7677afcd --- /dev/null +++ b/lib/utils/api_exception.dart @@ -0,0 +1,14 @@ +/// Exception that can be thrown by the [SignetsApi] +class ApiException implements Exception { + final String message; + final String prefix; + final String errorCode; + + const ApiException( + {this.prefix = "", this.message = "", this.errorCode = ""}); + + @override + String toString() { + return "$prefix ${errorCode.isNotEmpty ? "Code: $errorCode" : ""} Message: $message"; + } +} diff --git a/lib/utils/api_response.dart b/lib/utils/api_response.dart new file mode 100644 index 000000000..77ddd8a7c --- /dev/null +++ b/lib/utils/api_response.dart @@ -0,0 +1,24 @@ +class ApiResponse { + final T? data; + final String? error; + + ApiResponse({this.data, this.error}); + + factory ApiResponse.fromJson( + Map json, T Function(Map) fromJsonT) { + return ApiResponse( + data: json['data'] != null + ? fromJsonT(json['data'] as Map) + : null, + error: json['error'] as String?, + ); + } + + Map toJson(Map Function(T) toJsonT) { + return { + // ignore: null_check_on_nullable_type_parameter + 'data': data != null ? toJsonT(data!) : null, + 'error': error, + }; + } +} diff --git a/lib/utils/calendar_utils.dart b/lib/utils/calendar_utils.dart index 52992d7fe..d4d395bc1 100644 --- a/lib/utils/calendar_utils.dart +++ b/lib/utils/calendar_utils.dart @@ -1,7 +1,12 @@ +// Dart imports: import 'dart:collection'; + +// Package imports: import 'package:device_calendar/device_calendar.dart'; -import 'package:ets_api_clients/models.dart'; +// Project imports: +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; mixin CalendarUtils { static Future checkPermissions() async { diff --git a/lib/utils/command.dart b/lib/utils/command.dart new file mode 100644 index 000000000..e89343d6f --- /dev/null +++ b/lib/utils/command.dart @@ -0,0 +1,3 @@ +abstract class Command { + Future execute(); +} diff --git a/lib/utils/http_exception.dart b/lib/utils/http_exception.dart new file mode 100644 index 000000000..595dd5fe1 --- /dev/null +++ b/lib/utils/http_exception.dart @@ -0,0 +1,20 @@ +/// Exception for the different Rest API we can call +class HttpException implements Exception { + final String _message; + final String _prefix; + final int _code; + + HttpException({String prefix = "", int code = 404, String message = ""}) + : _message = message, + _code = code, + _prefix = prefix; + + int get code { + return _code; + } + + @override + String toString() { + return "$_prefix - $_code $_message"; + } +} diff --git a/lib/utils/locator.dart b/lib/utils/locator.dart index 2281a5021..b36b9f8aa 100644 --- a/lib/utils/locator.dart +++ b/lib/utils/locator.dart @@ -1,29 +1,31 @@ // Package imports: -import 'package:ets_api_clients/clients.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; -import 'package:notredame/features/app/repository/author_repository.dart'; -import 'package:notredame/features/app/repository/news_repository.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/app/repository/quick_link_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/widgets/app_widget_service.dart'; -import 'package:notredame/features/app/integration/github_api.dart'; -import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/error/internal_info_service.dart'; +import 'package:notredame/features/app/integration/github_api.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; -import 'package:notredame/features/app/storage/preferences_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/presentation/rive_animation_service.dart'; +import 'package:notredame/features/app/repository/author_repository.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/app/repository/quick_link_repository.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/app/storage/siren_flutter_service.dart'; +import 'package:notredame/features/app/widgets/app_widget_service.dart'; +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; GetIt locator = GetIt.instance; diff --git a/pubspec.lock b/pubspec.lock index b75c5c4b6..4ea9e2861 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -297,15 +297,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - ets_api_clients: - dependency: "direct main" - description: - path: "." - ref: "1.2.5" - resolved-ref: "8f7c94d960c7cdec751bf9845804b4a27f806c38" - url: "https://github.com/ApplETS/ETS-API-Clients.git" - source: git - version: "1.2.5" fake_async: dependency: transitive description: @@ -503,14 +494,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - flutter_keychain: - dependency: "direct main" - description: - name: flutter_keychain - sha256: de6f2d09ce2a006013db799ad37d6ae49b10530d6841de1c0c0525c4d3ec22a4 - url: "https://pub.dev" - source: hosted - version: "2.4.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -743,7 +726,7 @@ packages: source: hosted version: "0.15.4" http: - dependency: "direct main" + dependency: transitive description: name: http sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba @@ -810,10 +793,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -850,26 +833,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lint: dependency: "direct dev" description: @@ -922,10 +905,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -999,7 +982,7 @@ packages: source: hosted version: "1.0.1" path_provider: - dependency: "direct main" + dependency: transitive description: name: path_provider sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 @@ -1399,10 +1382,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" + sha256: "4ca32b2fc919452c9974abd4c6ea611a63e33b9e4f0b8c38dba3ac1f4a6549d1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.1.2" term_glyph: dependency: transitive description: @@ -1415,10 +1398,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" timeago: dependency: "direct main" description: @@ -1583,10 +1566,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d4758bc4..26d0d52f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ description: The 4th generation of ÉTSMobile, the main gateway between the Éco # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 4.43.0+1 +version: 4.44.0+1 environment: sdk: '>=3.3.0 <4.0.0' @@ -40,16 +40,7 @@ dependencies: flutter_staggered_animations: ^1.1.1 flutter_siren_2: ^1.2.2 - # Customs - # TODO: Change to the latest version, once reviewed and accepted - ets_api_clients: - #path: ../ETS-API-Clients/ - git: - url: https://github.com/ApplETS/ETS-API-Clients.git - ref: 1.2.5 - # Other - http: ^1.2.0 flutter_cache_manager: ^3.0.1 flutter_secure_storage: ^9.0.0 shared_preferences: ^2.2.2 @@ -59,7 +50,6 @@ dependencies: github: ^9.1.0 package_info_plus: ^5.0.1 feature_discovery: ^0.14.0 - path_provider: ^2.0.11 rive: ^0.13.1 connectivity_plus: ^6.0.1 flutter_svg: ^2.0.9 @@ -74,7 +64,6 @@ dependencies: calendar_view: ^1.1.0 carousel_slider: ^4.2.1 reorderable_grid_view: ^2.2.8 - flutter_keychain: ^2.4.0 shimmer: ^3.0.0 infinite_scroll_pagination: ^3.2.0 device_calendar: ^4.3.1 diff --git a/test/helpers.dart b/test/helpers.dart index 34f2181a6..63306583a 100644 --- a/test/helpers.dart +++ b/test/helpers.dart @@ -3,34 +3,34 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/testing.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:logger/logger.dart'; -import 'package:notredame/features/app/repository/author_repository.dart'; -import 'package:notredame/features/app/repository/news_repository.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/app/repository/quick_link_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/widgets/app_widget_service.dart'; -import 'package:notredame/features/app/integration/github_api.dart'; -import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/app/analytics/remote_config_service.dart'; import 'package:notredame/features/app/error/internal_info_service.dart'; +import 'package:notredame/features/app/integration/github_api.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; -import 'package:notredame/features/app/storage/preferences_service.dart'; -import 'package:notredame/features/app/analytics/remote_config_service.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/presentation/rive_animation_service.dart'; +import 'package:notredame/features/app/repository/author_repository.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/app/repository/quick_link_repository.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/features/app/storage/preferences_service.dart'; import 'package:notredame/features/app/storage/siren_flutter_service.dart'; +import 'package:notredame/features/app/widgets/app_widget_service.dart'; +import 'package:notredame/features/more/feedback/in_app_review_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/utils/locator.dart'; import 'mock/managers/author_repository_mock.dart'; import 'mock/managers/cache_manager_mock.dart'; @@ -46,11 +46,13 @@ import 'mock/services/github_api_mock.dart'; import 'mock/services/in_app_review_service_mock.dart'; import 'mock/services/internal_info_service_mock.dart'; import 'mock/services/launch_url_service_mock.dart'; +import 'mock/services/mon_ets_api_mock.dart'; import 'mock/services/navigation_service_mock.dart'; import 'mock/services/networking_service_mock.dart'; import 'mock/services/preferences_service_mock.dart'; import 'mock/services/remote_config_service_mock.dart'; import 'mock/services/rive_animation_service_mock.dart'; +import 'mock/services/signets_api_mock.dart'; import 'mock/services/siren_flutter_service_mock.dart'; /// Return the path of the [goldenName] file. diff --git a/test/managers/course_repository_test.dart b/test/managers/course_repository_test.dart index 581da2785..8b744a53a 100644 --- a/test/managers/course_repository_test.dart +++ b/test/managers/course_repository_test.dart @@ -2,24 +2,32 @@ import 'dart:convert'; // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; -import 'package:ets_api_clients/testing.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:intl/intl.dart'; import 'package:mockito/mockito.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; import 'package:notredame/features/app/repository/course_repository.dart'; import 'package:notredame/features/app/repository/user_repository.dart'; -import 'package:notredame/features/app/analytics/analytics_service.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/utils/activity_code.dart'; +import 'package:notredame/utils/api_exception.dart'; import '../helpers.dart'; import '../mock/managers/cache_manager_mock.dart'; import '../mock/managers/user_repository_mock.dart'; import '../mock/services/analytics_service_mock.dart'; import '../mock/services/networking_service_mock.dart'; +import '../mock/services/signets_api_mock.dart'; void main() { late AnalyticsServiceMock analyticsServiceMock; diff --git a/test/managers/quick_link_repository_test.dart b/test/managers/quick_link_repository_test.dart index e9b9a5b26..7f27380ce 100644 --- a/test/managers/quick_link_repository_test.dart +++ b/test/managers/quick_link_repository_test.dart @@ -9,8 +9,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/app/repository/quick_link_repository.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link_data.dart'; import '../helpers.dart'; diff --git a/test/managers/settings_manager_test.dart b/test/managers/settings_manager_test.dart index 82a0d646e..1dce2fd4f 100644 --- a/test/managers/settings_manager_test.dart +++ b/test/managers/settings_manager_test.dart @@ -11,8 +11,8 @@ import 'package:table_calendar/table_calendar.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/storage/preferences_service.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import '../helpers.dart'; import '../mock/services/analytics_service_mock.dart'; import '../mock/services/preferences_service_mock.dart'; diff --git a/test/managers/user_repository_test.dart b/test/managers/user_repository_test.dart index fa3d074a6..1bcf3b840 100644 --- a/test/managers/user_repository_test.dart +++ b/test/managers/user_repository_test.dart @@ -5,24 +5,29 @@ import 'dart:convert'; import 'package:flutter/services.dart'; // Package imports: -import 'package:ets_api_clients/clients.dart'; -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; -import 'package:ets_api_clients/testing.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; // Project imports: -import 'package:notredame/features/app/storage/cache_manager.dart'; -import 'package:notredame/features/app/repository/user_repository.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/features/app/storage/cache_manager.dart'; +import 'package:notredame/utils/api_exception.dart'; +import 'package:notredame/utils/http_exception.dart'; import '../helpers.dart'; import '../mock/managers/cache_manager_mock.dart'; import '../mock/services/analytics_service_mock.dart'; import '../mock/services/flutter_secure_storage_mock.dart'; +import '../mock/services/mon_ets_api_mock.dart'; import '../mock/services/networking_service_mock.dart'; +import '../mock/services/signets_api_mock.dart'; void main() { late AnalyticsServiceMock analyticsServiceMock; diff --git a/test/mock/managers/author_repository_mock.dart b/test/mock/managers/author_repository_mock.dart index a3f7c42a1..be304ce82 100644 --- a/test/mock/managers/author_repository_mock.dart +++ b/test/mock/managers/author_repository_mock.dart @@ -1,10 +1,10 @@ // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: import 'package:notredame/features/app/repository/author_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; import 'author_repository_mock.mocks.dart'; // Project imports: diff --git a/test/mock/managers/course_repository_mock.dart b/test/mock/managers/course_repository_mock.dart index cef12cc0e..1ec047204 100644 --- a/test/mock/managers/course_repository_mock.dart +++ b/test/mock/managers/course_repository_mock.dart @@ -1,11 +1,14 @@ // Package imports: -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'course_repository_mock.mocks.dart'; @GenerateNiceMocks([MockSpec()]) diff --git a/test/mock/managers/news_repository_mock.dart b/test/mock/managers/news_repository_mock.dart index b5781dc22..f882e9864 100644 --- a/test/mock/managers/news_repository_mock.dart +++ b/test/mock/managers/news_repository_mock.dart @@ -1,11 +1,11 @@ // Package imports: -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'news_repository_mock.mocks.dart'; @GenerateNiceMocks([MockSpec()]) diff --git a/test/mock/managers/quick_links_repository_mock.dart b/test/mock/managers/quick_links_repository_mock.dart index ec459e8e4..6137e64e6 100644 --- a/test/mock/managers/quick_links_repository_mock.dart +++ b/test/mock/managers/quick_links_repository_mock.dart @@ -1,5 +1,4 @@ // Package imports: -import 'package:ets_api_clients/exceptions.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -7,6 +6,7 @@ import 'package:mockito/mockito.dart'; import 'package:notredame/features/app/repository/quick_link_repository.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link.dart'; import 'package:notredame/features/ets/quick-link/models/quick_link_data.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'quick_links_repository_mock.mocks.dart'; @GenerateNiceMocks([MockSpec()]) diff --git a/test/mock/managers/user_repository_mock.dart b/test/mock/managers/user_repository_mock.dart index 5688a0469..f1e903cd7 100644 --- a/test/mock/managers/user_repository_mock.dart +++ b/test/mock/managers/user_repository_mock.dart @@ -1,11 +1,13 @@ // Package imports: -import 'package:ets_api_clients/exceptions.dart'; -import 'package:ets_api_clients/models.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; import 'package:notredame/features/app/repository/user_repository.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/utils/api_exception.dart'; import 'user_repository_mock.mocks.dart'; /// Mock for the [UserRepository] diff --git a/test/mock/services/github_api_mock.dart b/test/mock/services/github_api_mock.dart index 7dcbda5b3..3c65ddf81 100644 --- a/test/mock/services/github_api_mock.dart +++ b/test/mock/services/github_api_mock.dart @@ -8,8 +8,8 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; // Project imports: -import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'package:notredame/features/app/integration/github_api.dart'; +import 'package:notredame/features/more/feedback/models/feedback_issue.dart'; import 'github_api_mock.mocks.dart'; /// Mock for the [GithubApi] diff --git a/test/mock/services/mon_ets_api_mock.dart b/test/mock/services/mon_ets_api_mock.dart new file mode 100644 index 000000000..6df747bc2 --- /dev/null +++ b/test/mock/services/mon_ets_api_mock.dart @@ -0,0 +1,43 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +// Project imports: +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/utils/http_exception.dart'; +import 'mon_ets_api_mock.mocks.dart'; + +// UTILS + +/// Mock of the [MonETSApiClient] +@GenerateNiceMocks([MockSpec()]) +class MonETSAPIClientMock extends MockMonETSAPIClient { + /// Stub the user to return when a authenticate is called using the username + /// of [userToReturn] + static void stubAuthenticate( + MonETSAPIClientMock mock, MonETSUser userToReturn) { + when(mock.authenticate( + username: userToReturn.username, password: anyNamed('password'))) + .thenAnswer((_) async => userToReturn); + } + + /// Stub to throw an [HttpException] when the authenticate + /// will be called with this [username] + static void stubAuthenticateException( + MonETSAPIClientMock mock, String username) { + when(mock.authenticate(username: username, password: anyNamed('password'))) + .thenThrow(HttpException(code: 500, prefix: MonETSAPIClient.tagError)); + } + + /// Stub to throw an [Exception] when the authenticate + /// will be called with this [username] + static void stubException(MonETSAPIClientMock mock, String username, + {Exception? exception}) { + exception ??= Exception(); + when(mock.authenticate(username: username, password: anyNamed('password'))) + .thenThrow(exception); + } +} diff --git a/test/mock/services/signets_api_mock.dart b/test/mock/services/signets_api_mock.dart new file mode 100644 index 000000000..337a7a36c --- /dev/null +++ b/test/mock/services/signets_api_mock.dart @@ -0,0 +1,182 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/utils/api_exception.dart'; +import 'signets_api_mock.mocks.dart'; + +// MODELS + +// SERVICE + +/// Mock for the [SignetsApi] +@GenerateNiceMocks([MockSpec()]) +class SignetsAPIClientMock extends MockSignetsAPIClient { + static const signetsException = ApiException(prefix: SignetsAPIClient.tag); + + // ignore: deprecated_member_use_from_same_package + /// Stub the answer of the [authenticate]. + static void stubAuthenticate(MockSignetsAPIClient mock, + {bool connected = false}) { + // ignore: deprecated_member_use_from_same_package + when(mock.authenticate( + username: anyNamed("username"), password: anyNamed("password"))) + .thenAnswer((_) async => connected); + } + + /// Stub the answer of the [getCoursesActivities] when the [session] is used. + static void stubGetCoursesActivities(MockSignetsAPIClient mock, + String session, List coursesActivitiesToReturn) { + when(mock.getCoursesActivities( + username: anyNamed("username"), + password: anyNamed("password"), + session: session)) + .thenAnswer((_) async => coursesActivitiesToReturn); + } + + /// Throw [exceptionToThrow] when [getCoursesActivities] with the [session] is used. + static void stubGetCoursesActivitiesException( + MockSignetsAPIClient mock, String session, + {required ApiException exceptionToThrow}) { + when(mock.getCoursesActivities( + username: anyNamed("username"), + password: anyNamed("password"), + session: session)) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getCoursesActivities] when the [session] is used. + static void stubGetScheduleActivities(MockSignetsAPIClient mock, + String session, List coursesActivitiesToReturn) { + when(mock.getScheduleActivities( + username: anyNamed("username"), + password: anyNamed("password"), + session: session)) + .thenAnswer((_) async => coursesActivitiesToReturn); + } + + /// Throw [exceptionToThrow] when [getScheduleActivities] with the [session] is used. + static void stubGetScheduleActivitiesException( + MockSignetsAPIClient mock, String session, + {required ApiException exceptionToThrow}) { + when(mock.getScheduleActivities( + username: anyNamed("username"), + password: anyNamed("password"), + session: session)) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getSessions] when the [username] is used. + static void stubGetSessions(MockSignetsAPIClient mock, String username, + List sessionsToReturn) { + when(mock.getSessions(username: username, password: anyNamed("password"))) + .thenAnswer((_) async => sessionsToReturn); + } + + /// Throw [exceptionToThrow] when [getSessions] with the [username] is used. + static void stubGetSessionsException( + MockSignetsAPIClient mock, String username, + {ApiException exceptionToThrow = signetsException}) { + when(mock.getSessions(username: username, password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getPrograms] when the [username] is used. + static void stubGetPrograms(MockSignetsAPIClient mock, String username, + List programsToReturn) { + when(mock.getPrograms(username: username, password: anyNamed("password"))) + .thenAnswer((_) async => programsToReturn); + } + + /// Throw [exceptionToThrow] when [getPrograms] with the [username] is used. + static void stubGetProgramsException( + MockSignetsAPIClient mock, String username, + {ApiException exceptionToThrow = signetsException}) { + when(mock.getPrograms(username: username, password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getInfo] when the [username] is used. + static void stubGetInfo( + MockSignetsAPIClient mock, String username, ProfileStudent infoToReturn) { + when(mock.getStudentInfo( + username: username, password: anyNamed("password"))) + .thenAnswer((_) async => infoToReturn); + } + + /// Throw [exceptionToThrow] when [getInfo] with the [username] is used. + static void stubGetInfoException(MockSignetsAPIClient mock, String username, + {ApiException exceptionToThrow = signetsException}) { + when(mock.getStudentInfo( + username: username, password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getCourses] when the [username] is used. + static void stubGetCourses(MockSignetsAPIClient mock, String username, + {List coursesToReturn = const []}) { + when(mock.getCourses(username: username, password: anyNamed("password"))) + .thenAnswer((_) async => coursesToReturn); + } + + /// Throw [exceptionToThrow] when [getCourses] with the [username] is used. + static void stubGetCoursesException( + MockSignetsAPIClient mock, String username, + {ApiException exceptionToThrow = signetsException}) { + when(mock.getCourses(username: username, password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getCourseSummary] when the [username] and [course] is used. + static void stubGetCourseSummary( + MockSignetsAPIClient mock, String username, Course course, + {CourseSummary? summaryToReturn}) { + when(mock.getCourseSummary( + username: username, course: course, password: anyNamed("password"))) + .thenAnswer((_) async => summaryToReturn!); + } + + /// Throw [exceptionToThrow] when [getCourseSummary] with the [username] and [course] is used. + static void stubGetCourseSummaryException( + MockSignetsAPIClient mock, String username, Course course, + {ApiException exceptionToThrow = signetsException}) { + when(mock.getCourseSummary( + username: username, course: course, password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } + + /// Stub the answer of the [getCoursesReviews] when the [username] and [session] is used. + /// If [session] is null any session will be accepted. + static void stubGetCourseReviews(MockSignetsAPIClient mock, String username, + {Session? session, List reviewsToReturn = const []}) { + when(mock.getCourseReviews( + username: username, + session: session ?? anyNamed('session'), + password: anyNamed("password"))) + .thenAnswer((_) async => reviewsToReturn); + } + + /// Throw [exceptionToThrow] when [getCoursesReviews] with the [username] and [session] is used. + /// If [session] is null any session will be accepted. + static void stubGetCourseReviewsException( + MockSignetsAPIClient mock, String username, + {Session? session, ApiException exceptionToThrow = signetsException}) { + when(mock.getCourseReviews( + username: username, + session: session ?? anyNamed('session'), + password: anyNamed("password"))) + .thenThrow(exceptionToThrow); + } +} diff --git a/test/mock/signets-api-client/http_client_mock_helper.dart b/test/mock/signets-api-client/http_client_mock_helper.dart new file mode 100644 index 000000000..7ee95a641 --- /dev/null +++ b/test/mock/signets-api-client/http_client_mock_helper.dart @@ -0,0 +1,45 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:http/http.dart'; +import 'package:http/testing.dart'; + +/// Mock for the Http client +mixin HttpClientMockHelper { + /// Stub the next post of [url] and return [jsonResponse] with [statusCode] as http response code. + static MockClient stubJsonPost(String url, Map jsonResponse, + [int statusCode = 200]) { + return stubPost(url, jsonEncode(jsonResponse), statusCode); + } + + /// Stub the next post request to [url] and return [response] with [statusCode] as http response code. + static MockClient stubPost(String url, String response, + [int statusCode = 200]) { + return MockClient((Request r) { + if (r.method == 'POST' && r.url == Uri.parse(url)) { + return Future.value(Response(response, statusCode)); + } + return Future.value(Response('', 500)); + }); + } + + /// Stub the next get of [url] and return [jsonResponse] with [statusCode] as http response code. + static MockClient stubJsonGet(String url, Map jsonResponse, + [int statusCode = 200]) { + return stubGet(url, jsonEncode(jsonResponse), statusCode); + } + + /// Stub the next get request to [url] and return [response] with [statusCode] as http response code. + static MockClient stubGet(String url, String response, + [int statusCode = 200]) { + return MockClient((Request r) { + if (r.method == 'GET' && r.url == Uri.parse(url)) { + return Future.value(Response(response, statusCode)); + } + return Future.value(Response('', 500)); + }); + } +} diff --git a/test/services/app_widget_service_test.dart b/test/services/app_widget_service_test.dart index 91d07d278..d4ada8b5e 100644 --- a/test/services/app_widget_service_test.dart +++ b/test/services/app_widget_service_test.dart @@ -4,8 +4,8 @@ import 'package:home_widget/home_widget.dart'; // Project imports: import 'package:notredame/constants/widget_helper.dart'; -import 'package:notredame/features/student/grades/widget_models.dart'; import 'package:notredame/features/app/widgets/app_widget_service.dart'; +import 'package:notredame/features/student/grades/widget_models.dart'; import '../helpers.dart'; import '../mock/services/home_widget_mock.dart'; diff --git a/test/services/hello_api_client_test.dart b/test/services/hello_api_client_test.dart new file mode 100644 index 000000000..1d721107b --- /dev/null +++ b/test/services/hello_api_client_test.dart @@ -0,0 +1,207 @@ +// Package imports: +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:http/testing.dart'; + +// Project imports: +import 'package:notredame/features/ets/events/api-client/hello_api_client.dart'; +import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; +import 'package:notredame/features/ets/events/api-client/models/report.dart'; +import 'package:notredame/utils/api_response.dart'; +import 'package:notredame/utils/http_exception.dart'; +import '../mock/signets-api-client/http_client_mock_helper.dart'; + +void main() { + const String helloNewsAPI = "api.hello.ca"; + late HelloAPIClient service; + late MockClient mockClient; + + group('HelloApi - ', () { + setUp(() { + // default response stub + mockClient = MockClient((request) => Future.value(Response("", 200))); + + service = HelloAPIClient(client: mockClient); + service.apiLink = helloNewsAPI; + }); + + tearDown(() { + mockClient.close(); + }); + + group('getEvents - ', () { + test('empty data', () async { + final query = { + 'pageNumber': 1.toString(), + 'pageSize': 10.toString(), + }; + final uri = Uri.https(helloNewsAPI, '/api/events', query); + mockClient = HttpClientMockHelper.stubJsonGet(uri.toString(), { + 'data': [], + 'pageNumber': 1, + 'pageSize': 10, + 'totalPages': 1, + 'totalRecords': 0 + }); + service = buildService(mockClient); + + final result = await service.getEvents(); + + expect(result, isA()); + expect(result.news.length, 0); + }); + + test('one news', () async { + final news = News( + id: "402e711c-0f72-4aab-9684-31f1956c1da1", + title: "title", + content: "content", + imageUrl: "imageUrl", + state: "1", + publicationDate: DateTime.now(), + eventStartDate: DateTime.now().add(const Duration(days: 4)), + eventEndDate: DateTime.now().add(const Duration(days: 4, hours: 2)), + createdAt: DateTime.now().subtract(const Duration(days: 4)), + updatedAt: DateTime.now().subtract(const Duration(days: 4)), + tags: [], + organizer: Organizer( + id: "3a5cb049-67cf-428e-b98f-ef29fb633e0d", + organization: "name2", + email: "email2", + type: "organizer", + activityArea: ActivityArea( + id: "1", + nameFr: "Fr", + nameEn: "En", + createdAt: DateTime.now().subtract(const Duration(days: 4)), + updatedAt: DateTime.now().subtract(const Duration(days: 4))), + )); + + final query = { + 'pageNumber': 1.toString(), + 'pageSize': 10.toString(), + }; + final uri = Uri.https(helloNewsAPI, '/api/events', query); + mockClient = HttpClientMockHelper.stubJsonGet(uri.toString(), { + 'data': [news], + 'pageNumber': 1, + 'pageSize': 10, + 'totalPages': 1, + 'totalRecords': 1 + }); + service = buildService(mockClient); + + final result = await service.getEvents(); + + expect(result, isA()); + expect(result.news.length, 1); + expect(result.news[0].id, "402e711c-0f72-4aab-9684-31f1956c1da1"); + }); + + test('any other errors for now', () async { + const int statusCode = 500; + const String message = "An error has occurred."; + + mockClient = HttpClientMockHelper.stubJsonPost( + helloNewsAPI, {"Message": message}, statusCode); + service = buildService(mockClient); + + expect(service.getEvents(), throwsA(isA())); + }); + }); + }); + + group('getOrganizer - ', () { + test('successful response', () async { + const organizerId = '1234'; + final organizer = Organizer( + id: organizerId, + name: 'Test Organizer', + email: 'test@example.com', + avatarUrl: 'https://example.com/avatar.png', + type: 'type', + organization: 'Test Organization', + activityArea: ActivityArea( + id: "1", + nameFr: "Fr", + nameEn: "En", + createdAt: DateTime.now().subtract(const Duration(days: 4)), + updatedAt: DateTime.now().subtract(const Duration(days: 4))), + isActive: true, + profileDescription: 'Test Description', + facebookLink: 'https://facebook.com/test', + instagramLink: 'https://instagram.com/test', + tikTokLink: 'https://tiktok.com/test', + xLink: 'https://x.com/test', + discordLink: 'https://discord.com/test', + linkedInLink: 'https://linkedin.com/test', + redditLink: 'https://reddit.com/test', + webSiteLink: 'https://example.com', + ); + + final apiResponse = ApiResponse(data: organizer); + + final uri = Uri.https(helloNewsAPI, '/api/organizers/$organizerId'); + mockClient = HttpClientMockHelper.stubJsonGet(uri.toString(), + apiResponse.toJson((organizer) => organizer.toJson())); + service = buildService(mockClient); + + final result = await service.getOrganizer(organizerId); + + expect(result, isA()); + expect(result?.id, organizerId); + expect(result?.name, 'Test Organizer'); + }); + + test('error response', () async { + const organizerId = '1234'; + const int statusCode = 404; + const String message = "Organizer not found."; + + final uri = + Uri.https(helloNewsAPI, '/api/moderator/organizer/$organizerId'); + mockClient = HttpClientMockHelper.stubJsonGet( + uri.toString(), {"Message": message}, statusCode); + service = buildService(mockClient); + + expect(service.getOrganizer(organizerId), throwsA(isA())); + }); + }); + + final report = Report(reason: "Test reason", category: "1"); + + group('reportNews - ', () { + test('successful report', () async { + const newsId = '123'; + final uri = Uri.https(helloNewsAPI, '/api/reports/$newsId'); + mockClient = HttpClientMockHelper.stubJsonPost(uri.toString(), {}); + service = buildService(mockClient); + + final result = await service.reportNews(newsId, report); + + expect(result, isTrue); + }); + + test('error response', () async { + const newsId = '123'; + const int statusCode = 400; + const String message = "Error reporting news."; + + final uri = Uri.https(helloNewsAPI, '/api/events/$newsId/reports'); + mockClient = HttpClientMockHelper.stubJsonPost( + uri.toString(), {"Message": message}, statusCode); + service = buildService(mockClient); + + expect(service.reportNews(newsId, report), throwsA(isA())); + }); + }); +} + +HelloAPIClient buildService(MockClient client) { + final apiClient = HelloAPIClient(client: client); + apiClient.apiLink = "api.hello.ca"; + return apiClient; +} diff --git a/test/services/monets_api_client_test.dart b/test/services/monets_api_client_test.dart new file mode 100644 index 000000000..46afafdaa --- /dev/null +++ b/test/services/monets_api_client_test.dart @@ -0,0 +1,74 @@ +// Dart imports: +import 'dart:convert'; + +// Package imports: +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:http/testing.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/monets_api/models/mon_ets_user.dart'; +import 'package:notredame/features/app/monets_api/monets_api_client.dart'; +import 'package:notredame/utils/http_exception.dart'; +import '../mock/signets-api-client/http_client_mock_helper.dart'; + +void main() { + late MonETSAPIClient service; + late MockClient mockClient; + + group('MonETSApi - ', () { + setUp(() { + // default response stub + mockClient = MockClient((request) => Future.value(Response("", 200))); + + service = MonETSAPIClient(client: mockClient); + }); + + tearDown(() { + mockClient.close(); + }); + group('authentication - ', () { + test('right credentials', () async { + const String username = "username"; + const String password = "password"; + + mockClient = HttpClientMockHelper.stubJsonPost( + Urls.authenticationMonETS, + {'Domaine': 'domaine', 'TypeUsagerId': 1, 'Username': username}); + service = buildService(mockClient); + + mockClient = MockClient((request) async { + return Response( + jsonEncode({ + "Domaine": "domaine", + "TypeUsagerId": 1, + "Username": username + }), + 200); + }); + + final result = + await service.authenticate(username: username, password: password); + + expect(result, isA()); + expect(result.username, username); + }); + + test('wrong credentials / any other errors for now', () async { + const int statusCode = 500; + const String message = "An error has occurred."; + + mockClient = HttpClientMockHelper.stubJsonPost( + Urls.authenticationMonETS, {"Message": message}, statusCode); + service = buildService(mockClient); + + expect(service.authenticate(username: "", password: ""), + throwsA(isA())); + }); + }); + }); +} + +MonETSAPIClient buildService(MockClient client) => + MonETSAPIClient(client: client); diff --git a/test/services/signets_api_client_test.dart b/test/services/signets_api_client_test.dart new file mode 100644 index 000000000..b419043d2 --- /dev/null +++ b/test/services/signets_api_client_test.dart @@ -0,0 +1,930 @@ +// FLUTTER / DART / THIRD-PARTIES + +// Package imports: +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:intl/intl.dart'; + +// Project imports: +import 'package:notredame/constants/urls.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/course_evaluation.dart'; +import 'package:notredame/features/app/signets-api/models/course_review.dart'; +import 'package:notredame/features/app/signets-api/models/course_summary.dart'; +import 'package:notredame/features/app/signets-api/models/profile_student.dart'; +import 'package:notredame/features/app/signets-api/models/program.dart'; +import 'package:notredame/features/app/signets-api/models/schedule_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/signets-api/models/signets_errors.dart'; +import 'package:notredame/features/app/signets-api/signets_api_client.dart'; +import 'package:notredame/utils/activity_code.dart'; +import 'package:notredame/utils/api_exception.dart'; +import '../mock/signets-api-client/http_client_mock_helper.dart'; + +void main() { + late MockClient clientMock; + late SignetsAPIClient service; + + final session = Session( + shortName: 'H2018', + name: 'Hiver 2018', + startDate: DateTime(2018, 1, 4), + endDate: DateTime(2018, 4, 23), + endDateCourses: DateTime(2018, 4, 11), + startDateRegistration: DateTime(2017, 10, 30), + deadlineRegistration: DateTime(2017, 11, 14), + startDateCancellationWithRefund: DateTime(2018, 1, 4), + deadlineCancellationWithRefund: DateTime(2018, 1, 17), + deadlineCancellationWithRefundNewStudent: DateTime(2018, 1, 31), + startDateCancellationWithoutRefundNewStudent: DateTime(2018, 2), + deadlineCancellationWithoutRefundNewStudent: DateTime(2018, 3, 14), + deadlineCancellationASEQ: DateTime(2018, 1, 31)); + + group('SignetsApi - ', () { + setUp(() { + service = + buildService(MockClient((_) => Future.value(http.Response('', 500)))); + }); + + tearDown(() { + // Clear the mock and all interactions not already processed + clientMock.close(); + }); + + group("getCoursesActivities - ", () { + const String courseActivityXML = '' + '2020-09-03T18:00:00 ' + '2020-09-03T20:00:00 ' + 'GEN101-01 ' + 'TP ' + 'À distance ' + 'Travaux pratiques ' + 'Libelle du cours ' + ''; + + final courseActivity = CourseActivity( + courseGroup: 'GEN101-01', + courseName: 'Libelle du cours', + activityName: 'TP', + activityDescription: 'Travaux pratiques', + activityLocation: 'À distance', + startDateTime: DateTime(2020, 9, 3, 18), + endDateTime: DateTime(2020, 9, 3, 20)); + + test("right credentials and valid parameters", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildResponse( + Urls.listClassScheduleOperation, + courseActivityXML + courseActivityXML, + 'ListeDesSeances'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getCoursesActivities( + username: username, + password: password, + session: session, + startDate: DateTime(2020, 9, 3, 18), + endDate: DateTime(2020, 9, 3, 20)); + + expect(result, isA>()); + expect(result.first == courseActivity, isTrue); + expect(result.length, 2); + }); + + /// This occur when register for a internship without any other courses + /// or when the schedule for the desired session isn't available yet. + test("no courses activities available for the session", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildErrorResponse( + Urls.listClassScheduleOperation, + SignetsError.scheduleNotAvailable, + 'ListeDesSeances'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getCoursesActivities( + username: username, password: password, session: session); + + expect(result, isA>()); + expect(result.length, 0); + + final String stubResponseF = buildErrorResponse( + Urls.listClassScheduleOperation, + SignetsError.scheduleNotAvailable, + 'ListeDesSeances'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponseF); + service = buildService(clientMock); + + final resultF = await service.getCoursesActivities( + username: username, password: password, session: session); + + expect(resultF, isA>()); + expect(resultF.length, 0); + }); + + group("invalid parameters - ", () { + test("session", () async { + const String username = "username"; + const String password = "password"; + const String session = "A202"; + + expect( + service.getCoursesActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "The session should validate the regex: /^([A-E-H][0-9]{4})/"); + }); + + test("courseGroup", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + const String courseGroup1 = "MA123-01"; + const String courseGroup2 = "MAT12-01"; + const String courseGroup3 = "MAT12301"; + const String courseGroup4 = "MAT123-1"; + + expect( + service.getCoursesActivities( + username: username, + password: password, + session: session, + courseGroup: courseGroup1), + throwsA(isA()), + reason: + "A courseGroup should validate the regex: /^([A-Z]{3}[0-9]{3}-[0-9]{2})/"); + expect( + service.getCoursesActivities( + username: username, + password: password, + session: session, + courseGroup: courseGroup2), + throwsA(isA()), + reason: + "A courseGroup should validate the regex: /^([A-Z]{3}[0-9]{3}-[0-9]{2})/"); + expect( + service.getCoursesActivities( + username: username, + password: password, + session: session, + courseGroup: courseGroup3), + throwsA(isA()), + reason: + "A courseGroup should validate the regex: /^([A-Z]{3}[0-9]{3}-[0-9]{2})/"); + expect( + service.getCoursesActivities( + username: username, + password: password, + session: session, + courseGroup: courseGroup4), + throwsA(isA()), + reason: + "A courseGroup should validate the regex: /^([A-Z]{3}[0-9]{3}-[0-9]{2})/"); + }); + + test("startDate is after endDate", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final DateTime startDate = DateTime(2020, 2); + final DateTime endDate = DateTime(2020); + + expect( + service.getCoursesActivities( + username: username, + password: password, + session: session, + startDate: startDate, + endDate: endDate), + throwsArgumentError, + reason: "The startDate should be before the endDate"); + }); + }); + + test("An error occurred", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildErrorResponse( + Urls.listClassScheduleOperation, + 'An error occurred', + 'ListeDesSeances'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getCoursesActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + + test("Wrong credentials", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildErrorResponse( + Urls.listClassScheduleOperation, + SignetsError.credentialsInvalid, + 'ListeDesSeances'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getCoursesActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + group("getScheduleActivities - ", () { + const String scheduleActivityXML = """ + + GEN101 + 01 + 1 + Lundi + M + Laboratoire aux 2 semaines + Non + 08:30 + 12:30 + À distance + Generic title + + """; + + final scheduleActivity = ScheduleActivity( + courseAcronym: 'GEN101', + courseGroup: '01', + dayOfTheWeek: 1, + day: 'Lundi', + activityCode: ActivityCode.labEvery2Weeks, + name: 'Laboratoire aux 2 semaines', + isPrincipalActivity: false, + startTime: DateFormat('HH:mm').parse("08:30"), + endTime: DateFormat('HH:mm').parse("12:30"), + activityLocation: 'À distance', + courseTitle: 'Generic title', + ); + + test("right credentials and valid parameters", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildResponse(Urls.listeHoraireEtProf, + scheduleActivityXML + scheduleActivityXML, 'listeActivites'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getScheduleActivities( + username: username, password: password, session: session); + + expect(result, isA>()); + expect(result.first == scheduleActivity, isTrue); + expect(result.length, 2); + }); + + test("Wrong credentials", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildErrorResponse(Urls.listeHoraireEtProf, + SignetsError.credentialsInvalid, 'listeActivites'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getScheduleActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + + test("An error occurred", () async { + const String username = "username"; + const String password = "password"; + const String session = "A2020"; + + final String stubResponse = buildErrorResponse( + Urls.listeHoraireEtProf, 'An error occurred', 'listeActivites'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getScheduleActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + + group("invalid parameters - ", () { + test("session", () async { + const String username = "username"; + const String password = "password"; + const String session = "A202"; + + expect( + service.getScheduleActivities( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "The session should validate the regex: /^([A-E-H][0-9]{4})/"); + }); + }); + }); + + group("getSessions - ", () { + const String sessionXML = '' + 'H2018' + 'Hiver 2018 ' + '2018-01-04' + '2018-04-23' + '2018-04-11' + '2017-10-30' + '2017-11-14' + '2018-01-04' + '2018-01-17' + '2018-01-31' + '2018-02-01' + '2018-03-14' + '2018-01-31' + ''; + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildResponse( + Urls.listSessionsOperation, sessionXML + sessionXML, 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = + await service.getSessions(username: username, password: password); + + expect(result, isA>()); + expect(result.first == session, isTrue); + expect(result.length, 2); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildErrorResponse( + Urls.listSessionsOperation, 'An error occurred', 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect(service.getSessions(username: username, password: password), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + + group("getStudentInfo - ", () { + const String studentInfoXML = 'Doe' + 'John' + 'DOEJ00000000' + '99.99' + '1'; + + final studentInfo = ProfileStudent( + lastName: 'Doe', + firstName: 'John', + permanentCode: 'DOEJ00000000', + balance: '99.99'); + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = + buildResponse(Urls.infoStudentOperation, studentInfoXML); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getStudentInfo( + username: username, password: password); + + expect(result, isA()); + expect(result == studentInfo, isTrue); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = + buildErrorResponse(Urls.infoStudentOperation, 'An error occurred'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect(service.getStudentInfo(username: username, password: password), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + + group("getPrograms - ", () { + const String programXML = '' + '9999' + 'Genie' + 'Etudiant' + 'Actif' + '2020-01-04' + '2020-04-04' + '3' + '7' + '6' + '5' + '4' + '3' + '2' + '1' + ''; + + final program = Program( + name: 'Genie', + code: '9999', + average: '3', + accumulatedCredits: '3', + registeredCredits: '4', + completedCourses: '6', + failedCourses: '5', + equivalentCourses: '7', + status: 'Actif'); + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildResponse( + Urls.listProgramsOperation, programXML + programXML, 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = + await service.getPrograms(username: username, password: password); + + expect(result, isA>()); + expect(result.first == program, isTrue); + expect(result.length, 2); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildErrorResponse( + Urls.listProgramsOperation, 'An error occurred', 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect(service.getPrograms(username: username, password: password), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + + group("getCourses - ", () { + const String courseWithGradeXML = '' + 'GEN101' + '02' + 'H2020' + '999' + 'C+' + '3' + 'Cours générique ' + ''; + + final courseWithGrade = Course( + acronym: 'GEN101', + group: '02', + session: 'H2020', + programCode: '999', + grade: 'C+', + numberOfCredits: 3, + title: 'Cours générique'); + + const String courseWithoutGradeXML = '' + 'GEN101' + '02' + 'H2020' + '999' + ' ' + '3' + 'Cours générique ' + ''; + + final Course courseWithoutGrade = Course( + acronym: 'GEN101', + group: '02', + session: 'H2020', + programCode: '999', + numberOfCredits: 3, + title: 'Cours générique'); + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildResponse(Urls.listCourseOperation, + courseWithGradeXML + courseWithoutGradeXML, 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = + await service.getCourses(username: username, password: password); + + expect(result, isA>()); + expect(result[0], courseWithGrade); + expect(result[1], courseWithoutGrade); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildErrorResponse( + Urls.listCourseOperation, 'An error occurred', 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect(service.getCourses(username: username, password: password), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + + group("getCourseSummary - ", () { + final Course course = Course( + acronym: 'GEN101', + group: '02', + session: 'H2020', + programCode: '999', + numberOfCredits: 3, + title: 'Cours générique'); + + final courseSummary = CourseSummary( + currentMark: 5, + currentMarkInPercent: 50, + markOutOf: 10, + passMark: 6, + standardDeviation: 2.3, + median: 4.5, + percentileRank: 99, + evaluations: [ + CourseEvaluation( + courseGroup: 'GEN101-01', + title: 'Test', + correctedEvaluationOutOf: "20", + weight: 10, + published: false, + teacherMessage: '', + ignore: false), + CourseEvaluation( + courseGroup: 'GEN101-02', + title: 'Test', + mark: 18, + correctedEvaluationOutOf: "20+10", + weight: 10, + passMark: 16, + standardDeviation: 6.4, + median: 15.3, + percentileRank: 99, + published: true, + teacherMessage: 'Je suis content', + ignore: false, + targetDate: DateTime(2020)) + ]); + + const String courseSummaryXml = '50' + '5' + '6' + '2,3' + '4,5' + '99,0' + '10' + '' + '' + 'GEN101-01' + 'Test' + ' ' + ' ' + ' ' + '20' + '10' + ' ' + ' ' + ' ' + ' ' + 'Non' + ' ' + 'Non ' + '' + '' + 'GEN101-02' + 'Test' + 'Test' + '2020-01-01 ' + '18' + '20+10' + '10' + '16 ' + '6,4' + '15.3' + '99' + 'Oui' + 'Je suis content ' + 'Non ' + '' + ''; + + const String courseSummaryEmptyXml = ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '0,0 ' + ''; + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = + buildResponse(Urls.listEvaluationsOperation, courseSummaryXml); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getCourseSummary( + username: username, password: password, course: course); + + expect(result, isA()); + expect(result, courseSummary); + + expect(result.evaluations[0].weightedGrade, null); + expect(result.evaluations[1].weightedGrade, 9.0); + }); + + test("Summary is empty", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = + buildResponse(Urls.listEvaluationsOperation, courseSummaryEmptyXml); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getCourseSummary( + username: username, password: password, course: course), + throwsA(isA()), + reason: + "If the summary is empty, the service should return an error."); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildErrorResponse( + Urls.listEvaluationsOperation, 'An error occurred'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getCourseSummary( + username: username, password: password, course: course), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + + group("getCoursesEvaluation - ", () { + const String courseReviewCompletedXML = ' ' + 'GEN101 ' + '01 ' + 'April, Alain ' + '2021-03-19T00:00:00 ' + '2021-03-28T23:59:00 ' + 'Cours ' + 'true ' + ''; + + const String incompleteCourseReviewForSameCourseXML = ' ' + 'GEN101 ' + '01 ' + 'Another, Teacher ' + '2021-03-19T00:00:00 ' + '2021-03-28T23:59:00 ' + 'Cours ' + 'false ' + ''; + + const String courseReviewNotCompletedXML = ' ' + 'GEN102 ' + '01 ' + 'April, Alain ' + '2021-03-19T00:00:00 ' + '2021-03-28T23:59:00 ' + 'Cours ' + 'false ' + ''; + + final courseReviewCompleted = CourseReview( + acronym: 'GEN101', + group: '01', + teacherName: 'April, Alain', + startAt: DateTime(2021, 03, 19), + endAt: DateTime(2021, 03, 28, 23, 59), + type: 'Cours', + isCompleted: true); + + final incompleteCourseReviewForSameCourse = CourseReview( + acronym: 'GEN101', + group: '01', + teacherName: 'Another, Teacher', + startAt: DateTime(2021, 03, 19), + endAt: DateTime(2021, 03, 28, 23, 59), + type: 'Cours', + isCompleted: false); + + final courseReviewNotCompleted = CourseReview( + acronym: 'GEN102', + group: '01', + teacherName: 'April, Alain', + startAt: DateTime(2021, 03, 19), + endAt: DateTime(2021, 03, 28, 23, 59), + type: 'Cours', + isCompleted: false); + + test("right credentials", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildResponse( + Urls.readCourseReviewOperation, + courseReviewCompletedXML + + incompleteCourseReviewForSameCourseXML + + courseReviewNotCompletedXML, + 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + final result = await service.getCourseReviews( + username: username, password: password, session: session); + + expect(result, isA>()); + expect( + result, + containsAll([ + courseReviewCompleted, + incompleteCourseReviewForSameCourse, + courseReviewNotCompleted + ])); + }); + + // Currently SignetsAPI doesn't have a clear way to indicate which error + // occurred (no error code, no change of http code, just a text) + // so for now whatever the error we will throw a generic error + test("wrong credentials / an error occurred", () async { + const String username = "username"; + const String password = "password"; + + final String stubResponse = buildErrorResponse( + Urls.readCourseReviewOperation, 'An error occurred', 'liste'); + + clientMock = + HttpClientMockHelper.stubPost(Urls.signetsAPI, stubResponse); + service = buildService(clientMock); + + expect( + service.getCourseReviews( + username: username, password: password, session: session), + throwsA(isA()), + reason: + "If the SignetsAPI return an error the service should return the error."); + }); + }); + }); +} + +SignetsAPIClient buildService(MockClient client) => + SignetsAPIClient(client: client); + +String buildResponse(String operation, String body, [String? firstElement]) => + ' ' + ' ' + ' ' + '<${operation}Response xmlns="http://etsmtl.ca/"> ' + '<${operation}Result>' + ' ' + '${firstElement != null ? '<$firstElement>' : ''}' + '$body' + '${firstElement != null ? '' : ''}' + '' + '' + '' + ''; + +String buildErrorResponse(String operation, String error, + [String? firstElement]) => + ' ' + ' ' + '' + '<${operation}Response xmlns="http://etsmtl.ca/"> ' + '<${operation}Result>' + '' + '$error' + '' + '[<$firstElement />]? ' + '' + '' + '' + ''; diff --git a/test/services/soap_service_test.dart b/test/services/soap_service_test.dart new file mode 100644 index 000000000..0b4463013 --- /dev/null +++ b/test/services/soap_service_test.dart @@ -0,0 +1,32 @@ +// Package imports: +import 'package:flutter_test/flutter_test.dart'; + +// Project imports: +import 'package:notredame/features/app/signets-api/soap_service.dart'; + +void main() { + group('SoapService - ', () { + test('buildBasicSoapBody - contains all basic element', () { + const String username = "username"; + const String password = "password"; + + // ignore: missing_whitespace_between_adjacent_strings + const expectedResult = '' + // ignore: missing_whitespace_between_adjacent_strings + '' + '' + // ignore: missing_whitespace_between_adjacent_strings + '' + 'username' + 'password' + '' + '' + ''; + + final result = + SoapService.buildBasicSOAPBody("testOperation", username, password); + + expect(result.buildDocument().toString(), expectedResult); + }); + }); +} diff --git a/test/ui/views/author_view_test.dart b/test/ui/views/author_view_test.dart index 17f79c486..d8a5af854 100644 --- a/test/ui/views/author_view_test.dart +++ b/test/ui/views/author_view_test.dart @@ -2,7 +2,6 @@ import 'dart:io'; // Flutter imports: -import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; // Package imports: @@ -10,16 +9,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; // Project imports: -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/app/repository/news_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; import 'package:notredame/features/app/integration/launch_url_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/repository/news_repository.dart'; +import 'package:notredame/features/ets/events/api-client/models/activity_area.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; import 'package:notredame/features/ets/events/author/author_view.dart'; import 'package:notredame/features/ets/events/news/widgets/news_card.dart'; import 'package:notredame/features/ets/events/social/social_links_card.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import '../../helpers.dart'; import '../../mock/managers/author_repository_mock.dart'; import '../../mock/managers/news_repository_mock.dart'; diff --git a/test/ui/views/choose_language_view_test.dart b/test/ui/views/choose_language_view_test.dart index f20cbd9f9..2f4711b41 100644 --- a/test/ui/views/choose_language_view_test.dart +++ b/test/ui/views/choose_language_view_test.dart @@ -9,9 +9,9 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; // Project imports: -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/navigation/navigation_service.dart'; import 'package:notredame/features/more/settings/choose_language_view.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import '../../helpers.dart'; void main() { diff --git a/test/ui/views/dashboard_view_test.dart b/test/ui/views/dashboard_view_test.dart index 839e761c8..ba59a1c26 100644 --- a/test/ui/views/dashboard_view_test.dart +++ b/test/ui/views/dashboard_view_test.dart @@ -5,7 +5,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; // Package imports: -import 'package:ets_api_clients/models.dart'; import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,9 +12,12 @@ import 'package:flutter_test/flutter_test.dart'; // Project imports: import 'package:notredame/constants/preferences_flags.dart'; import 'package:notredame/constants/update_code.dart'; +import 'package:notredame/features/app/signets-api/models/course.dart'; +import 'package:notredame/features/app/signets-api/models/course_activity.dart'; +import 'package:notredame/features/app/signets-api/models/session.dart'; +import 'package:notredame/features/app/widgets/dismissible_card.dart'; import 'package:notredame/features/dashboard/dashboard_view.dart'; import 'package:notredame/features/dashboard/widgets/course_activity_tile.dart'; -import 'package:notredame/features/app/widgets/dismissible_card.dart'; import 'package:notredame/features/student/grades/widgets/grade_button.dart'; import '../../helpers.dart'; import '../../mock/managers/course_repository_mock.dart'; diff --git a/test/ui/views/ets_view_test.dart b/test/ui/views/ets_view_test.dart index 508dc96be..140ec5860 100644 --- a/test/ui/views/ets_view_test.dart +++ b/test/ui/views/ets_view_test.dart @@ -1,7 +1,7 @@ -// Flutter imports: +// Dart imports: import 'dart:io'; -import 'package:ets_api_clients/models.dart'; +// Flutter imports: import 'package:flutter/material.dart'; // Package imports: @@ -9,15 +9,19 @@ import 'package:feature_discovery/feature_discovery.dart'; import 'package:flutter_test/flutter_test.dart'; // Project imports: -import 'package:notredame/features/app/repository/course_repository.dart'; -import 'package:notredame/features/app/repository/news_repository.dart'; -import 'package:notredame/features/more/settings/settings_manager.dart'; import 'package:notredame/features/app/analytics/analytics_service.dart'; -import 'package:notredame/features/app/navigation/navigation_service.dart'; -import 'package:notredame/features/app/integration/networking_service.dart'; import 'package:notredame/features/app/analytics/remote_config_service.dart'; -import 'package:notredame/features/ets/ets_view.dart'; +import 'package:notredame/features/app/integration/networking_service.dart'; +import 'package:notredame/features/app/navigation/navigation_service.dart'; +import 'package:notredame/features/app/repository/course_repository.dart'; +import 'package:notredame/features/app/repository/news_repository.dart'; import 'package:notredame/features/app/widgets/base_scaffold.dart'; +import 'package:notredame/features/ets/ets_view.dart'; +import 'package:notredame/features/ets/events/api-client/models/news.dart'; +import 'package:notredame/features/ets/events/api-client/models/news_tags.dart'; +import 'package:notredame/features/ets/events/api-client/models/organizer.dart'; +import 'package:notredame/features/ets/events/api-client/models/paginated_news.dart'; +import 'package:notredame/features/more/settings/settings_manager.dart'; import '../../helpers.dart'; import '../../mock/managers/news_repository_mock.dart'; import '../../mock/services/remote_config_service_mock.dart'; diff --git a/test/ui/views/faq_view_test.dart b/test/ui/views/faq_view_test.dart index 7a6190daa..39713bc5c 100644 --- a/test/ui/views/faq_view_test.dart +++ b/test/ui/views/faq_view_test.dart @@ -10,8 +10,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; // Project imports: -import 'package:notredame/features/more/faq/models/faq.dart'; import 'package:notredame/features/more/faq/faq_view.dart'; +import 'package:notredame/features/more/faq/models/faq.dart'; import '../../helpers.dart'; import '../../mock/managers/settings_manager_mock.dart'; diff --git a/test/ui/views/goldenFiles/newsDetailsView_1.png b/test/ui/views/goldenFiles/newsDetailsView_1.png index bec28632d0a34e0dfc47ac06ed2b19fb126eccac..1bfdf664fbd76e1fd7c1d51cd29d3c3d5188bb44 100644 GIT binary patch literal 5228 zcmb_gcT|(vwht&)D_&RZ{QWhLjF@7w#g_xbH|PJ)HG5f_Ir2M7e> zGBGx=0)bdKfiIc;ATV={URD77u!L9{>4VBUkIevs10nh*HtfJBg5CWA2*f8~V(<@} z+lcvLc<5t#4snU(t}H3^Db7IR;4eoF77}?%To=3Yiv!WO1?@5XhJKB zX45phiL4ggokWq*d~qcqMYs`CP+^t+xUa5z5UL#LB3(jNVq*G|lweIqhBCTw?A*e| zAI^*}M&&uww4Of*Cp;>AMd5V+-t?AAkzLE5{@83-{yu*^4h1=Tc_rvnU9gyh;7xY^ z?`!$vl_V9(UnpF-&c`X!YMh{xZ}-I{cwrr-w`^K5sMC6irfVhloGxFN7IOrhKK@_9 z@0mu@5oz@TgBTcDcIq8xOBeFDw13+FwihKi7sy&0P@M5Ozw-!z_!U?X`Oz^Mh^7w~ z5g~V7l?byNT36HLVM`rcxmcE4Q=#&%N=o%Y&XC{U_xAKI92@ zJw&8Qg4`Q5Q~u6-|3OLI%(0Ef9nHd_5UJm6lxiQdR zh`ty=NiaCBMSpA!hTKI)a!?QKc+uYSn4?|98wf3FlN<_!`>^Y(`pp*p2!4qn@1 z6V|)WmS1zPX^HL5)x(7M{UmMvnpA&W&8RFlwzG9CDoRlb0xTFu+q3~lb8}tBy^vj6 zEX@9RghT7n+#D#_5P_2X18KW9dwqO;OYO7?tr*8O^vjnQs(5dJUD%pOiujXW=9p>i zd{P<8_Q3kr)74#B)*t?fh(BWb`5^hXcTK3r&)Yt+y|TH^Q{&lRnw>37qJTcXPUSV# z+zUSHyPx%840n0m!j-t=tR~SN8@-6fkKo>4&ijxq{av;Mrb&VXuDY|waQ%U!5zS%< zP_V!s=KqSy@W6equ5p`#)PczSk>>2F@dDttfW!)s|0juyEov0V^DgJ?kSMiyuqKPU z#*fkYUp8p%>w#-60G^xwwmmYda8R?a?<|!*+iq_|2*YGQzg1F&G3)5)NX#^_y>bOz zQK8)1)APM};EGvUPO(np<~lgoZoEeFgNa}LV5RqxE0|6}^;Fj@l(-Qe3KejYkR=0$ zs>I9~B(P@mIa-a0I3wv?bh(rNj%627A!3m`8`kovFlFlw5a_Aa<4d)f+Bh3d@heY5 zrFC9=b_fqIPl%U<>0`>XBcB`**8Dsrb2ot^pQ@6bogKQ9tl_;&`-VD}s1)wk>zl0x z&&-Wna1?yWspA@pconr14%yf#wINiG^sa>6x<&0gt4oO_5L^-rT6;s-%m#4A^i4M3 zSxRB$NcN_h3?^$9F>{PcR1>=sEIz%hTr~kuYydkno#hO3qznaO0G(F5P4ge$r0A4A z4`?(L6&20Jq|M)UkJTH*xy3Kww;4Ld>}QH)tIO?8)Zjco=dd;E0k%sLbl zgq%WJT3N$)|j)q!2p@tZnyh za%52+uZY23R*G;Qur-}h7Ma9L&jc|q?99}0H(_)H*1pY4PU%5qfCy&DK4~4dbA)@~cc{oeA;MGW_1dTY$gXcyUm}!oZa1cofT&wf7GJ4w>LX1TbKFTsKByA zo3_t|yvB>uyB0Z}ZyEuMU3w`#R0s%qh^1-(^hvx>u-PxFYi$UFy(eqa5G$%1efWo% zEHl7VOu%Rpe&Up_E!MeF+r@Vo2gT2I(ZZX|s{vFEX9|p{x%J<|H}6cG^~xz2jE<<+ z&r9|4bw3Bh$LX7Wr#qMo=I8I01rTt+_3L^51iCup3^kaaZ<3*#wcW({AmicTf!`Wk z=<0oqwes=t8JK<^`bm0c_ZEY?KJn<*b^O;Are9!8QtQ|&PsEw?)=g@L+`5|&GMOHA z17S5MppO6qzT{g16GKwYP7|->mZO9qKA0lkL(aEw&IwmNFhImPM>NdQow@I+?3M zb1jD?02=*`{;kiBJBD>|mgUv&TD|Ms=FMz-;=F$O*`8&u_jtv23dFgWSkM1hhg!uD zG2DH3dG$Jft}FXjA_q`_1OODx_kY)n0@+?1&=rF){87(;=zsoIi~tlMc>u+q)Vkl- zG@b!97)Jo(;O7EkNX{FnpY?&&cLu1gFmTSBKN5~Fot7_Nz#2@bQ#u9Fzu6mG<=4Hd z3c-U5q@my?FDTQc(RW&|X^w~A#=bsGN2w_#;r)gAIwlLHb6VYXEdNd{8``@)p***H zX3l1;?+ft9&-qi|&ifl}?;H#s-=C_`q?HT(deNp7PwoIySg_Hm*Yod7ggK?u{p8~!RhixqX;AU#` zb$P)|Pv@hzJOX_Y8VS$FpAwgg1Bd&m@mVn}d3z!!f05lDC6su#DaVDW!Cwoc8Yv4T zt*-~oHUwuP($hc5|Eg%gIF!N9ynqSsAY2~n4e;`WNEpGylPruOUYG3WR%gn=Q^xnL zt@6Q?Xct4%Oqq&RleQpjC%G@LY#}7=>DjX@&^IFnZ|v+zf^d(baS;t*tFY>G9iDlR z4Akv4EKx?ra{9?@K5^a7(i7=C&O6n6n({8MKaop)2yka&_<2R^_4$o4s3R}OOgBnj z!&xfsO;pOh1CP9L-y2qspQs3=($^Qy_rR7e5s09D#0-bGFOm5RZ5a)kRdV>tPs{pN zt+&PYv-45hei2u>2=O8XZdI03wcz*15k`1NvJNvUXmYq@xn2is=bGn|H1`S#AXCudn1 ziqw9WY`u}cH7S$0O@CI&Abypd?U!T!-}MP8qXV0H4ZI4L5yuEZEaBr9%uzCHf%9wis@oaw7a}3vw5DHHTEf;-c2ckv9^m;HMoI&J~8nh#_ugywBub zfo@~zX{QKUxU6rW;0?_dd(i*r)*Q1|@k&D|J_TItLOz9X9+ zo3$SssafCBE8TDfQ)ayZ8%i3UEvV|6GV(bm!G)Eu4oE$83SB4k^tb4_*#QjIo{1WD zb1woO&s#dQ!o2ee&Ja&oSW#!b_Nx+R%(XYGZg_0O%T*=QsP=`TNjZMB+B0lTh4UE- zTM>Ed^8&e>u&KuC=yk9@+2ou+gWVVxF`<;6p#UEF)>w@Z%09_lY}`Rtx_#*#{`UUZ zx-c>{YG?vkv+Qd{T2!4Zy$#eHUdm|8hDTNWNP1V0M)T^;#Qd;J zVdzyf16Sqtdc@BG(S;MJd34srBW=V)T%)Y=0(DU5(q?T-V`)`M6tjc0BQ+0 zGj}zpWQ0g|s9(4`G0e~rgo!1k-Br9eQ)`fKx_=Fc7!M*AReGXQ&L*XWlCecRTdde-{(iy+?RN*#o6qlZN;1fJSSYp3< zF?@b>0?DA-rsbI|UuKT$0G%GR>yhD9l+@7E{VqI;zu7sqy$}mfDIfoO*eoOZy=`De zx00!@K+)S)&d4>9a*N_=agxKXK0IH7uj^OE)NOtrqH7|)r z{1w;$q#7B^l9T!Vtw~)mxJC}#2=MyWs~ir6<^pQjh+-q4Qtwt$Q?G?VP`Uu=^s+8;{0M^()Wt{p9KFRk^(t$%t%uBjX=D7xW4`XY0UYxbd9J!}(U^{b+t zP9myVhBEowM)0`|NM;eZGBf>i13# zJHFcem6({=juXf2T*SmSNPwePd@H!(zw)pYylsebaX2bg{YqsDoNSIddcsW{yfEVD zQ^dr+zH`Fvh#RVKj)Bc8jEQSmq%kJcuPD6F+)a+m`Z`f;I?GV$W=gLY{CK4OyPXoH zJ+EqQ?%EK(U2#|sv2eS4@XkoC{(aZ`0~&5APC~tBa67qMSD@-=Y>!JV+g{2Yf3^8~ z2ZOWce4hVxng})D-w@XrG+#3~xfXA%%$z0l3&RJWm^4<&h;4g)nbV)mAtsCM-%1d3 zY2A2yr-b@3ohx!*oZLJRDE_iTXg=)FPWPEj_}cx05PY<#w|)@%w}YI8l?GYX_#O0^)H<7_*khGFb*U_ zY&RJh$~y=@qIa0lI41b+tmT&TkDgRnTE)D4N1+*DFB(?w?)O<<^d2l)LC*@1(a&}=XOx0v^X|J#(t-eX$>413@Ezf%yI!(a$&B$^4STRZ)?}AADz3qYEenBTDPSevF z={$s_HD=rjY{qDxjWM^dkg&ui08M2v`?=8Jb4p4Nd`S^LMavNZis5*@kG?6VljrhtMnt-qFq0n$$DFVqTc zGJk8cWiu$aGnM++JnH*N_T{5(TEDG04f*eW1O*|npKb3;`2>2dKl^O^|1l)(H;LP> z1U)WpwpJ7MJ44&rcM)Rz)svEwp(vE8h6aT~DS7ZfJ9BC(^>Wl?+YAk3TlkvpzSP<# zBhd$J8upYQ=6)~<7%R)*H+Ic48lW@1RfKQ`1vUQ0VeAQnq1FhjQ2B>YemYjjH>OS& z@vR$u9|cTYBQK_zv>DJ2yLK-sn}ns)7z_1W7Fwj8^AF-)`A)yr@3(hM7)f|BnT_bN zq=NAD2Q_OUUB#%19YP_zOG~5|PM)GFDl5ZU61tA)goJ`+4e#mMlgiI!3@=0tH_wW= zeLziWadB}HzCkhu7+un?xj*F^N%z4r$k^EGoRtCdK|pBbyP7|c*&t}w-rqUqL5q91 z0w1C$i9}>JaUWqi^{ml)y=v#RZ0*sv9XYMwdQu^)L;`ZH{iR2bwSo`o3K5H82@|Yde0?Q3SdPzI)3nAxh6HVJ z?;3HDQ9{o0%TiqHrVh0}7OSRN#LF{@@O7N@B?KJ%N8CyemW`CwL8~)Sz0bGvYxWj- zzILHfsiQfA;kseM!6pnk6h`kbknn0>#V!avntzDnaU$Yoyj|<~BhuJB3WE|e6R`GE z$Cxgblf#zmI;sN0AWQHLa+q_$@lU67*%P7%$p;Fng(apw6YD)mGZtjiY#0R>F%a>ulx8 z5r^=)1|A3EzBeM=3A{LH>;bL}8223}od;E+k&)j0nomQPb9DdxL^|NzK)My0qXRxY z+fOQ=-MPQ^35f0*MEZp(c|oYqmNTp>m|8zIJnu7CK$K!5Sh>>{O^~Kb!0n2tX17!p zNpYo2#|!KxkO$=<*>S-^DR)wwcHA2>&)$7&{#rh+8Oe?<(@) zh@Xv8I@*C6d!7gfZX;FLK85*omFYLVy~f{IAODcQMYoj2ZOV&kFC+pfX62o!^Q8?P zYNZ!Gg@8%mK1E%MsgK~tudcBw*ZPo+%L0nErI$7h1UQ19R@e5>lXTJ>+uNBhyCkz0 zgk5iLZf>zy8uO@j1tKa)JLTQr8*rq<6O5~e+jDoxqB`!=?G5We2_COYs>!oUsIIAU z++b|^?N~1X<9Y`*tWg8iV4@4-XR5;6&bk|pYfDIzo}0f zHCRXZyf}Ksm_@oX@wl!I$jkFr^zcA{!rh#kwf4P9s70s-9u*l(;ZI^!vAWqenVEHuH`mK&bL1!C#w=E+2g6`$k?C_pP%zzqTBz; zEg(7N*??qWX!*r<_qW{uNRvt+!Cd-sOYmh$nISKMCS|DxGhr$ZA}x1WAgVJMnuE%X zt#|eAK`*FW=FXZfrS$8SPG;!Ac%zh8byI84Ja{t^K&7fPaBC#E1WPCFxD|cLyncq8 zXHW%^kH)Lyc3MHo5GpR59}WLHW}(dC$@oR^+~xVI1Om1WYxn*C(0c@qu+A#{qp zWY60&1Omt}#~gIROqf*+Mlc#R55=>oT;_*$Z4~jB8d9UCmGR_N^7PGByI{0K|CnI` zb@o|(c!z#*NYP9wKR3H*%$Jt!Ket%+PUWF_?cT1_+=rN9;SF?O7a}ui7RMW?}d1 zrgvoJE=xJ7-y31QAEIA%t*)3NhdOko7vFZKM-HXx)keiR!mNlNx{XxbXW~=_9$WRY z*Ml8_AdJ8WtJH0dyU!!L$E;8zl1$FU4RvZ}W|Sktt9j&s6K0vf5Xkav}?h#%W|Ny>tqw@D<* zPF6E=x=~v+<%&CmPAGFl>QMaj%6CRR!ew$Q^yVJPN}~$ssxfa(9lK+FkvCa~8SX;D zN0~GY72&lTrB!n+(lnSb@sJ(b+xsb1VQ(`SPgD<_mC50}vhRF;s<3xe^b_+#7j}t~ zC%ndl=aoIiME07&J%Dclac`ohGuvukIHwx~`ShlEqK*vfVbu)EA?6?Fq(bpBr`FlT zkESmTzFvxzo=JuI@#&z-&D~3Nvus%_IfhTU@u-__#7STT{^0=r^lz@m4O|=IQz}QRz9sC90+CVh>em1}vDv^&g0NA$kG7=X@|@dNm;9uuIG1AUgn z#-GdaZe`J@GeVQ`at^9`7D4PnRZ