From 8c34cc2bcb3ba985c178a868596ff70743303e57 Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 10 Jul 2023 19:15:47 -0400 Subject: [PATCH 01/19] Add ui --- l10n/intl_en.arb | 4 +- l10n/intl_fr.arb | 4 +- lib/ui/views/profile_view.dart | 346 +++++++++++++++++++++++----- lib/ui/widgets/student_program.dart | 138 ++++++++--- 4 files changed, 398 insertions(+), 94 deletions(-) diff --git a/l10n/intl_en.arb b/l10n/intl_en.arb index 8edb72ae5..c7b1cb53c 100644 --- a/l10n/intl_en.arb +++ b/l10n/intl_en.arb @@ -146,7 +146,7 @@ "profile_first_name": "First Name", "profile_last_name": "Last Name", "profile_permanent_code": "Permanent Code", - "profile_balance": "Balance", + "profile_balance": "My balance", "profile_code_program": "Code", "profile_average_program": "Average", "profile_number_accumulated_credits_program": "Accumulated Credits", @@ -155,6 +155,8 @@ "profile_number_failed_courses_program": "Failed Courses", "profile_number_equivalent_courses_program": "Equivalent Courses", "profile_status_program": "Status", + "profile_program_completion": "Program completion", + "profile_other_programs": "Other Programs", "ets_security_title": "Security", "ets_monets_title": "MonÉTS", diff --git a/l10n/intl_fr.arb b/l10n/intl_fr.arb index dfd485fe3..37cb9a867 100644 --- a/l10n/intl_fr.arb +++ b/l10n/intl_fr.arb @@ -144,7 +144,7 @@ "profile_first_name": "Prénom", "profile_last_name": "Nom de famille", "profile_permanent_code": "Code permanent", - "profile_balance": "Solde", + "profile_balance": "Votre solde", "profile_code_program": "Code", "profile_average_program": "Moyenne", "profile_number_accumulated_credits_program": "Crédits complétés", @@ -153,6 +153,8 @@ "profile_number_failed_courses_program": "Cours échoués", "profile_number_equivalent_courses_program": "Cours d'équivalence", "profile_status_program": "Statut", + "profile_program_completion": "Complétion du programme", + "profile_other_programs": "Autres Programmes", "ets_security_title": "Sécurité", "ets_monets_title": "MonÉTS", diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index d0385109f..d3793466d 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -1,11 +1,16 @@ // FLUTTER / DART / THIRD-PARTIES import 'package:flutter/material.dart'; +import 'package:notredame/ui/utils/app_theme.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:percent_indicator/percent_indicator.dart'; // VIEW-MODEL import 'package:notredame/core/viewmodels/profile_viewmodel.dart'; +// MODEL +import 'package:ets_api_clients/models.dart'; + // SERVICES import 'package:notredame/core/services/analytics_service.dart'; @@ -14,7 +19,6 @@ import 'package:notredame/ui/widgets/student_program.dart'; // UTILS import 'package:notredame/ui/utils/loading.dart'; -import 'package:notredame/ui/utils/app_theme.dart'; // OTHER import 'package:notredame/locator.dart'; @@ -35,68 +39,292 @@ class _ProfileViewState extends State { } @override - Widget build(BuildContext context) => - ViewModelBuilder.reactive( - viewModelBuilder: () => ProfileViewModel(intl: AppIntl.of(context)), - builder: (context, model, child) { - return RefreshIndicator( - onRefresh: () => model.refresh(), - child: Stack( - children: [ - ListView(padding: EdgeInsets.zero, children: [ - ListTile( - title: Text( - AppIntl.of(context).profile_student_status_title, - style: const TextStyle(color: AppTheme.etsLightRed), +Widget build(BuildContext context) => ViewModelBuilder.reactive( + viewModelBuilder: () => ProfileViewModel(intl: AppIntl.of(context)), + builder: (context, model, child) { + return RefreshIndicator( + onRefresh: () => model.refresh(), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 5.0), + child: SizedBox( + height: 90, + child: getMainInfoCard(model), + ), + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0), + child: getMyInfosCard(model, context), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 4.0), + child: getMyBalanceCard(model, context), + ), + ], ), ), - ListTile( - title: Text(AppIntl.of(context).profile_balance), - trailing: Text(model.profileStudent.balance), - ), - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - ListTile( - title: Text( - AppIntl.of(context).profile_personal_information_title, - style: const TextStyle(color: AppTheme.etsLightRed), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), + child: getProgramCompletion(model, context), + ), + ], ), ), - ListTile( - title: Text(AppIntl.of(context).profile_first_name), - trailing: Text(model.profileStudent.firstName), - ), - ListTile( - title: Text(AppIntl.of(context).profile_last_name), - trailing: Text(model.profileStudent.lastName), - ), - ListTile( - title: Text(AppIntl.of(context).profile_permanent_code), - trailing: Text(model.profileStudent.permanentCode)), - ListTile( - title: Text( - AppIntl.of(context).login_prompt_universal_code), - trailing: Text(model.universalAccessCode)), - ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - reverse: true, - physics: const ScrollPhysics(), - itemCount: model.programList.length, - itemBuilder: (BuildContext context, int index) { - return StudentProgram(model.programList[index]); - }, + ], + ), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + getCurrentProgramTile(model.programList[model.programList.length - 1], context), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0), + child: Text( + AppIntl.of(context).profile_other_programs, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed + ), + ), ), - ]), - if (model.isBusy) - buildLoading(isInteractionLimitedWhileLoading: false) - else - const SizedBox() - ], + ], + ), + Column( + children: [ + for (var i = 0; i < model.programList.length - 1; i++) + StudentProgram(model.programList[i]), + ], + ), + const SizedBox( + height: 10.0 + ), + if (model.isBusy) + buildLoading(isInteractionLimitedWhileLoading: false) + else + const SizedBox() + ], + ), + ), + ); + }, + ); +} + +Card getMainInfoCard(ProfileViewModel model) { + return Card( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Text( + '${model.profileStudent.firstName} ${model.profileStudent.lastName}', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), - ); - }); + ), + ), + const Padding( + padding: EdgeInsets.all(5.0), + child: Text( + 'Génie logiciel', + style: TextStyle( + fontSize: 16, + ), + ), + ), + ], + ), + ), + ); +} + +Card getMyInfosCard(ProfileViewModel model, BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context).profile_permanent_code, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + model.profileStudent.permanentCode, + style: const TextStyle(fontSize: 14), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context).login_prompt_universal_code, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0, bottom: 16.0), + child: Text( + model.universalAccessCode, + style: const TextStyle(fontSize: 14), + ), + ), + ], + ), + ); +} + +Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { + final balance = model.profileStudent.balance; + return Card( + color: double.parse(balance.substring(0, balance.length - 1).replaceAll(",", ".")) > 0 ? Colors.red : Colors.green, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context).profile_balance, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Center( + child: Text( + balance, + style: const TextStyle(fontSize: 20), + ), + ), + ), + ], + ), + ); +} + +Card getProgramCompletion(ProfileViewModel model, BuildContext context) { + return Card( + child: SizedBox( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + AppIntl.of(context).profile_program_completion, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), + child: Center(child: getLoadingIndicator()), + ) + ], + ), + ), + ); +} + +CircularPercentIndicator getLoadingIndicator() { + return CircularPercentIndicator( + animation: true, + animationDuration: 1100, + radius: 40, + lineWidth: 10, + percent: 0.88, + circularStrokeCap: CircularStrokeCap.round, + center: const Text( + "88.0", + style: TextStyle(fontSize: 20), + ), + progressColor: Colors.green, + ); +} + +Column getCurrentProgramTile(Program program, BuildContext context) { + final List dataTitles = [ + AppIntl.of(context).profile_code_program, + AppIntl.of(context).profile_average_program, + AppIntl.of(context).profile_number_accumulated_credits_program, + AppIntl.of(context).profile_number_registered_credits_program, + AppIntl.of(context).profile_number_completed_courses_program, + AppIntl.of(context).profile_number_failed_courses_program, + AppIntl.of(context).profile_number_equivalent_courses_program, + AppIntl.of(context).profile_status_program + ]; + + final List dataFetched = [ + program.code, + program.average, + program.accumulatedCredits, + program.registeredCredits, + program.completedCourses, + program.failedCourses, + program.equivalentCourses, + program.status + ]; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), + child: Text( + program.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed + ), + ), + ), + ...List.generate(dataTitles.length, (index) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dataTitles[index]), + Text(dataFetched[index]), + ], + ), + ); + }), + ], + ); + } diff --git a/lib/ui/widgets/student_program.dart b/lib/ui/widgets/student_program.dart index 161ecdfd7..577be5d0d 100644 --- a/lib/ui/widgets/student_program.dart +++ b/lib/ui/widgets/student_program.dart @@ -1,20 +1,54 @@ // FLUTTER / DART / THIRD-PARTIES +import 'dart:math'; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// MODEL +// MODELS import 'package:ets_api_clients/models.dart'; // OTHER +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:notredame/ui/utils/app_theme.dart'; -class StudentProgram extends StatelessWidget { +class StudentProgram extends StatefulWidget { final Program _program; - const StudentProgram(this._program); + @override + State createState() => _StudentProgramState(); +} + +class _StudentProgramState extends State + with TickerProviderStateMixin { + bool showProgramDetails = false; + AnimationController controller; + Animation rotateAnimation; + + @override + void initState() { + super.initState(); + + controller = AnimationController( + vsync: this, + duration: const Duration( + milliseconds: 200, + ), + value: 1.0); + + rotateAnimation = Tween(begin: pi, end: 0.0) + .animate(CurvedAnimation(parent: controller, curve: Curves.easeIn)); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + final bool isLightMode = + Theme.of(context).brightness == Brightness.light; + final List dataTitles = [ AppIntl.of(context).profile_code_program, AppIntl.of(context).profile_average_program, @@ -25,41 +59,79 @@ class StudentProgram extends StatelessWidget { AppIntl.of(context).profile_number_equivalent_courses_program, AppIntl.of(context).profile_status_program ]; + final List dataFetched = [ - _program.code, - _program.average, - _program.accumulatedCredits, - _program.registeredCredits, - _program.completedCourses, - _program.failedCourses, - _program.equivalentCourses, - _program.status + widget._program.code, + widget._program.average, + widget._program.accumulatedCredits, + widget._program.registeredCredits, + widget._program.completedCourses, + widget._program.failedCourses, + widget._program.equivalentCourses, + widget._program.status ]; - return Column( - children: [ - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - ListTile( - title: Text( - _program.name, - style: const TextStyle(color: AppTheme.etsLightRed), + + return Theme( + data: Theme.of(context).copyWith( + dividerColor: Colors.transparent, + unselectedWidgetColor: Colors.red, + colorScheme: ColorScheme.fromSwatch().copyWith(onSecondary: Colors.red,), + ), + child: ExpansionTile( + onExpansionChanged: (value) { + setState(() { + showProgramDetails = !showProgramDetails; + + if (showProgramDetails) { + controller.reverse(from: pi); + } else { + controller.forward(from: 0.0); + } + }); + }, + title: Text( + widget._program.name, + style: TextStyle( + color: isLightMode ? Colors.black : Colors.white, ), ), - ListView.builder( - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemBuilder: (context, index) { - return ListTile( - title: Text(dataTitles[index]), - trailing: Text(dataFetched[index]), + trailing: Padding( + padding: const EdgeInsets.only(top: 5.0), + child: AnimatedBuilder( + animation: rotateAnimation, + builder: (BuildContext context, Widget child) { + return Transform.rotate( + angle: rotateAnimation.value, + child: const Icon( + Icons.keyboard_arrow_down_sharp, + color: AppTheme.etsLightRed, + ), ); }, - itemCount: dataTitles.length) - ], + child: const Icon( + Icons.keyboard_arrow_down_sharp, + color: AppTheme.etsLightRed, + ), + ), + ), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(dataTitles.length, (index) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dataTitles[index]), + Text(dataFetched[index]), + ], + ), + ); + }), + ), + ], + ), ); } } From 0b3096e5e39b3d7198bfd1551ecce9a1f153647a Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 10 Jul 2023 20:05:51 -0400 Subject: [PATCH 02/19] Make program completion work --- l10n/intl_en.arb | 1 + l10n/intl_fr.arb | 1 + lib/core/constants/programs_credits.dart | 25 +++++++++++++++ lib/core/viewmodels/profile_viewmodel.dart | 32 +++++++++++++++++++ lib/ui/views/profile_view.dart | 36 +++++++++++++--------- 5 files changed, 80 insertions(+), 15 deletions(-) create mode 100644 lib/core/constants/programs_credits.dart diff --git a/l10n/intl_en.arb b/l10n/intl_en.arb index c7b1cb53c..0d68fc193 100644 --- a/l10n/intl_en.arb +++ b/l10n/intl_en.arb @@ -156,6 +156,7 @@ "profile_number_equivalent_courses_program": "Equivalent Courses", "profile_status_program": "Status", "profile_program_completion": "Program completion", + "profile_program_completion_not_available": "N/A", "profile_other_programs": "Other Programs", "ets_security_title": "Security", diff --git a/l10n/intl_fr.arb b/l10n/intl_fr.arb index 37cb9a867..8c302e39c 100644 --- a/l10n/intl_fr.arb +++ b/l10n/intl_fr.arb @@ -154,6 +154,7 @@ "profile_number_equivalent_courses_program": "Cours d'équivalence", "profile_status_program": "Statut", "profile_program_completion": "Complétion du programme", + "profile_program_completion_not_available": "ND", "profile_other_programs": "Autres Programmes", "ets_security_title": "Sécurité", diff --git a/lib/core/constants/programs_credits.dart b/lib/core/constants/programs_credits.dart new file mode 100644 index 000000000..4e2594c4b --- /dev/null +++ b/lib/core/constants/programs_credits.dart @@ -0,0 +1,25 @@ +class ProgramCredits { + Map programsCredits = { + "7625" : 117, + "7694" : 115, + "7084" : 116, + "7684" : 115, + "6556" : 117, + "6557" : 118, + "7086" : 116, + // TODO: Ajouter le code lorsque le programme sera en vigeur + // (https://www.etsmtl.ca/etudes/premier-cycle/Baccalaureat-informatique-distribuee) + // "À venir" : 90", + "5766" : 30, + "4567" : 30, + "4412" : 30, + "4563" : 30, + "4684" : 30, + "4329" : 30, + "4288" : 30, + "Maîtrise" : 45, + "Programme court" : 15, + "Certificat" : 30, + "DESS" : 30, + }; +} diff --git a/lib/core/viewmodels/profile_viewmodel.dart b/lib/core/viewmodels/profile_viewmodel.dart index b80d03aab..a7f674129 100644 --- a/lib/core/viewmodels/profile_viewmodel.dart +++ b/lib/core/viewmodels/profile_viewmodel.dart @@ -4,12 +4,18 @@ import 'package:stacked/stacked.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +// SERVICES +import 'package:notredame/core/services/analytics_service.dart'; + // MANAGERS import 'package:notredame/core/managers/user_repository.dart'; // MODELS import 'package:ets_api_clients/models.dart'; +// CONSTANTS +import 'package:notredame/core/constants/programs_credits.dart'; + // OTHERS import 'package:notredame/locator.dart'; @@ -17,6 +23,8 @@ class ProfileViewModel extends FutureViewModel> { /// Load the user final UserRepository _userRepository = locator(); + final AnalyticsService analyticsService = locator(); + /// Localization class of the application. final AppIntl _appIntl; @@ -38,6 +46,30 @@ class ProfileViewModel extends FutureViewModel> { ProfileViewModel({@required AppIntl intl}) : _appIntl = intl; + double get programProgression { + final ProgramCredits programCredits = ProgramCredits(); + final int numberOfCreditsCompleted = int.parse(programList[programList.length - 1].accumulatedCredits); + final String code = programList[programList.length - 1].code; + int percentage = 0; + bool foundMatch = false; + + programCredits.programsCredits.forEach((key, value) { + if (key == code || + programList[programList.length - 1].name.startsWith(key)) { + percentage = (numberOfCreditsCompleted / value * 100).round(); + foundMatch = true; + } + }); + + if (!foundMatch) { + final String programName = programList[programList.length - 1].name; + analyticsService.logEvent("profile_view", 'The program $programName (code: $code) does not match any program'); + percentage = 0; + } + + return percentage.toDouble(); + } + @override // ignore: type_annotate_public_apis void onError(error) { diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index d3793466d..764825af4 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -20,6 +20,9 @@ import 'package:notredame/ui/widgets/student_program.dart'; // UTILS import 'package:notredame/ui/utils/loading.dart'; +// CONSTANTS +import 'package:notredame/core/constants/programs_credits.dart'; + // OTHER import 'package:notredame/locator.dart'; @@ -251,30 +254,33 @@ Card getProgramCompletion(ProfileViewModel model, BuildContext context) { ), Padding( padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), - child: Center(child: getLoadingIndicator()), - ) + child: Center(child: getLoadingIndicator(model, context)) + ), ], ), ), ); } -CircularPercentIndicator getLoadingIndicator() { +CircularPercentIndicator getLoadingIndicator(ProfileViewModel model, BuildContext context) { + final double percentage = model.programProgression; + return CircularPercentIndicator( - animation: true, - animationDuration: 1100, - radius: 40, - lineWidth: 10, - percent: 0.88, - circularStrokeCap: CircularStrokeCap.round, - center: const Text( - "88.0", - style: TextStyle(fontSize: 20), - ), - progressColor: Colors.green, - ); + animation: true, + animationDuration: 1100, + radius: 40, + lineWidth: 10, + percent: percentage / 100, + circularStrokeCap: CircularStrokeCap.round, + center: Text( + percentage != 0 ? '$percentage%' : AppIntl.of(context).profile_program_completion_not_available, + style: const TextStyle(fontSize: 20), + ), + progressColor: Colors.green, + ); } + Column getCurrentProgramTile(Program program, BuildContext context) { final List dataTitles = [ AppIntl.of(context).profile_code_program, From 04a7ed1b3d76b926d1fc88dcf05d884a31cbc53a Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Wed, 26 Jul 2023 20:20:19 -0400 Subject: [PATCH 03/19] Fix bug --- lib/ui/views/profile_view.dart | 125 +++++++++++++++++---------------- 1 file changed, 64 insertions(+), 61 deletions(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 764825af4..fd3668505 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -92,7 +92,7 @@ Widget build(BuildContext context) => ViewModelBuilder.reactiv indent: 10, endIndent: 10, ), - getCurrentProgramTile(model.programList[model.programList.length - 1], context), + getCurrentProgramTile(model.programList, context), const Divider( thickness: 2, indent: 10, @@ -167,42 +167,41 @@ Card getMainInfoCard(ProfileViewModel model) { Card getMyInfosCard(ProfileViewModel model, BuildContext context) { return Card( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), - child: Text( - AppIntl.of(context).profile_permanent_code, - style: const TextStyle( - fontSize: 16, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 3.0), + child: Text( + AppIntl.of(context).profile_permanent_code, + style: const TextStyle( + fontSize: 16, + ), ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - model.profileStudent.permanentCode, - style: const TextStyle(fontSize: 14), + Center( + child: Text( + model.profileStudent.permanentCode, + style: const TextStyle(fontSize: 14), + ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0, left: 16.0, bottom: 3.0), - child: Text( - AppIntl.of(context).login_prompt_universal_code, - style: const TextStyle( - fontSize: 16, + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 3.0), + child: Text( + AppIntl.of(context).login_prompt_universal_code, + style: const TextStyle(fontSize: 16), ), ), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0, bottom: 16.0), - child: Text( - model.universalAccessCode, - style: const TextStyle(fontSize: 14), + Center( + child: Text( + model.universalAccessCode, + style: const TextStyle(fontSize: 14), + ), ), - ), - ], + ], + ), ), ); } @@ -228,7 +227,7 @@ Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { child: Center( child: Text( balance, - style: const TextStyle(fontSize: 20), + style: const TextStyle(fontSize: 18), ), ), ), @@ -280,9 +279,11 @@ CircularPercentIndicator getLoadingIndicator(ProfileViewModel model, BuildContex ); } +Column getCurrentProgramTile(List programList, BuildContext context) { + if (programList.isNotEmpty) { + final program = programList.last; -Column getCurrentProgramTile(Program program, BuildContext context) { - final List dataTitles = [ + final List dataTitles = [ AppIntl.of(context).profile_code_program, AppIntl.of(context).profile_average_program, AppIntl.of(context).profile_number_accumulated_credits_program, @@ -304,33 +305,35 @@ Column getCurrentProgramTile(Program program, BuildContext context) { program.status ]; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), - child: Text( - program.name, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.etsLightRed + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0), + child: Text( + program.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed + ), ), ), - ), - ...List.generate(dataTitles.length, (index) { - return Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(dataTitles[index]), - Text(dataFetched[index]), - ], - ), - ); - }), - ], - ); - + ...List.generate(dataTitles.length, (index) { + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(dataTitles[index]), + Text(dataFetched[index]), + ], + ), + ); + }), + ], + ); + } else { + return Column(); + } } From 04d9a0c87cb95a4f9c22d407871ccf8e9e92edb9 Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Wed, 26 Jul 2023 20:32:43 -0400 Subject: [PATCH 04/19] [CI UPDATE GOLDENS] --- test/ui/views/profile_view_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index 5ba85864d..0ecab1453 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -22,6 +22,7 @@ import '../../mock/managers/user_repository_mock.dart'; void main() { AppIntl intl; UserRepository userRepository; + group('Profile view - ', () { setUp(() async { intl = await setupAppIntl(); From 234a6f8814ff9e2fd2d0f6eddf96fce3b5f0d868 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Thu, 24 Aug 2023 20:19:22 -0400 Subject: [PATCH 05/19] Fix loading error --- lib/core/viewmodels/profile_viewmodel.dart | 35 ++-- lib/ui/views/profile_view.dart | 206 +++++++++++---------- 2 files changed, 131 insertions(+), 110 deletions(-) diff --git a/lib/core/viewmodels/profile_viewmodel.dart b/lib/core/viewmodels/profile_viewmodel.dart index a7f674129..853cd9754 100644 --- a/lib/core/viewmodels/profile_viewmodel.dart +++ b/lib/core/viewmodels/profile_viewmodel.dart @@ -48,25 +48,30 @@ class ProfileViewModel extends FutureViewModel> { double get programProgression { final ProgramCredits programCredits = ProgramCredits(); - final int numberOfCreditsCompleted = int.parse(programList[programList.length - 1].accumulatedCredits); - final String code = programList[programList.length - 1].code; int percentage = 0; - bool foundMatch = false; - programCredits.programsCredits.forEach((key, value) { - if (key == code || - programList[programList.length - 1].name.startsWith(key)) { - percentage = (numberOfCreditsCompleted / value * 100).round(); - foundMatch = true; - } - }); + if (programList.isNotEmpty) { + final int numberOfCreditsCompleted = + int.parse(programList[programList.length - 1].accumulatedCredits); + final String code = programList[programList.length - 1].code; + bool foundMatch = false; + + programCredits.programsCredits.forEach((key, value) { + if (key == code || + programList[programList.length - 1].name.startsWith(key)) { + percentage = (numberOfCreditsCompleted / value * 100).round(); + foundMatch = true; + } + }); - if (!foundMatch) { - final String programName = programList[programList.length - 1].name; - analyticsService.logEvent("profile_view", 'The program $programName (code: $code) does not match any program'); - percentage = 0; + if (!foundMatch) { + final String programName = programList[programList.length - 1].name; + analyticsService.logEvent("profile_view", + 'The program $programName (code: $code) does not match any program'); + percentage = 0; + } } - + return percentage.toDouble(); } diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index fd3668505..60736419b 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -1,9 +1,12 @@ // FLUTTER / DART / THIRD-PARTIES +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:notredame/ui/utils/app_theme.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:percent_indicator/percent_indicator.dart'; +import 'package:logger/logger.dart'; // VIEW-MODEL import 'package:notredame/core/viewmodels/profile_viewmodel.dart'; @@ -34,6 +37,8 @@ class ProfileView extends StatefulWidget { class _ProfileViewState extends State { final AnalyticsService _analyticsService = locator(); + final Logger _logger = locator(); + @override void initState() { super.initState(); @@ -42,96 +47,98 @@ class _ProfileViewState extends State { } @override -Widget build(BuildContext context) => ViewModelBuilder.reactive( - viewModelBuilder: () => ProfileViewModel(intl: AppIntl.of(context)), - builder: (context, model, child) { - return RefreshIndicator( - onRefresh: () => model.refresh(), - child: SingleChildScrollView( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 5.0), - child: SizedBox( - height: 90, - child: getMainInfoCard(model), + Widget build(BuildContext context) => + ViewModelBuilder.reactive( + viewModelBuilder: () => ProfileViewModel(intl: AppIntl.of(context)), + builder: (context, model, child) { + return RefreshIndicator( + onRefresh: () => model.refresh(), + child: SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 5.0), + child: SizedBox( + height: 90, + child: getMainInfoCard(model), + ), ), - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0), - child: getMyInfosCard(model, context), - ), - Padding( - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 4.0), - child: getMyBalanceCard(model, context), - ), - ], + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0), + child: getMyInfosCard(model, context), + ), + Padding( + padding: + const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 4.0), + child: getMyBalanceCard(model, context), + ), + ], + ), ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), - child: getProgramCompletion(model, context), - ), - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: + const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), + child: getProgramCompletion(model, context), + ), + ], + ), ), - ), - ], - ), - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - getCurrentProgramTile(model.programList, context), - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0), - child: Text( - AppIntl.of(context).profile_other_programs, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.etsLightRed + ], + ), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + getCurrentProgramTile(model.programList, context), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 16.0, top: 8.0, bottom: 8.0), + child: Text( + AppIntl.of(context).profile_other_programs, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed), ), ), - ), - ], - ), - Column( - children: [ - for (var i = 0; i < model.programList.length - 1; i++) - StudentProgram(model.programList[i]), - ], - ), - const SizedBox( - height: 10.0 - ), - if (model.isBusy) - buildLoading(isInteractionLimitedWhileLoading: false) - else - const SizedBox() - ], + ], + ), + Column( + children: [ + for (var i = 0; i < model.programList.length - 1; i++) + StudentProgram(model.programList[i]), + ], + ), + const SizedBox(height: 10.0), + if (model.isBusy) + buildLoading(isInteractionLimitedWhileLoading: false) + else + const SizedBox() + ], + ), ), - ), - ); - }, - ); + ); + }, + ); } Card getMainInfoCard(ProfileViewModel model) { @@ -207,9 +214,17 @@ Card getMyInfosCard(ProfileViewModel model, BuildContext context) { } Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { - final balance = model.profileStudent.balance; - return Card( - color: double.parse(balance.substring(0, balance.length - 1).replaceAll(",", ".")) > 0 ? Colors.red : Colors.green, + final stringBalance = model.profileStudent.balance; + var balance = 0.0; + + if (stringBalance.isNotEmpty) { + balance = double.parse(stringBalance + .substring(0, stringBalance.length - 1) + .replaceAll(",", ".")); + } + + return Card( + color: balance > 0 ? Colors.red : Colors.green, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -226,7 +241,7 @@ Card getMyBalanceCard(ProfileViewModel model, BuildContext context) { padding: const EdgeInsets.only(bottom: 16.0), child: Center( child: Text( - balance, + stringBalance, style: const TextStyle(fontSize: 18), ), ), @@ -252,16 +267,16 @@ Card getProgramCompletion(ProfileViewModel model, BuildContext context) { ), ), Padding( - padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), - child: Center(child: getLoadingIndicator(model, context)) - ), + padding: const EdgeInsets.only(top: 30.0, bottom: 37.0), + child: Center(child: getLoadingIndicator(model, context))), ], ), ), ); } -CircularPercentIndicator getLoadingIndicator(ProfileViewModel model, BuildContext context) { +CircularPercentIndicator getLoadingIndicator( + ProfileViewModel model, BuildContext context) { final double percentage = model.programProgression; return CircularPercentIndicator( @@ -272,7 +287,9 @@ CircularPercentIndicator getLoadingIndicator(ProfileViewModel model, BuildContex percent: percentage / 100, circularStrokeCap: CircularStrokeCap.round, center: Text( - percentage != 0 ? '$percentage%' : AppIntl.of(context).profile_program_completion_not_available, + percentage != 0 + ? '$percentage%' + : AppIntl.of(context).profile_program_completion_not_available, style: const TextStyle(fontSize: 20), ), progressColor: Colors.green, @@ -315,8 +332,7 @@ Column getCurrentProgramTile(List programList, BuildContext context) { style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, - color: AppTheme.etsLightRed - ), + color: AppTheme.etsLightRed), ), ), ...List.generate(dataTitles.length, (index) { From 4265dec7181317da29c8b98bcd1187878cbaf0a5 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Thu, 24 Aug 2023 21:19:01 -0400 Subject: [PATCH 06/19] Fixed loading --- lib/ui/views/profile_view.dart | 162 +++++++++++++++++---------------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 60736419b..9c1f9316f 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -56,83 +56,10 @@ class _ProfileViewState extends State { child: SingleChildScrollView( child: Column( children: [ - Padding( - padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 5.0), - child: SizedBox( - height: 90, - child: getMainInfoCard(model), - ), - ), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: - const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0), - child: getMyInfosCard(model, context), - ), - Padding( - padding: - const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 4.0), - child: getMyBalanceCard(model, context), - ), - ], - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: - const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), - child: getProgramCompletion(model, context), - ), - ], - ), - ), - ], - ), - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - getCurrentProgramTile(model.programList, context), - const Divider( - thickness: 2, - indent: 10, - endIndent: 10, - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 16.0, top: 8.0, bottom: 8.0), - child: Text( - AppIntl.of(context).profile_other_programs, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppTheme.etsLightRed), - ), - ), - ], - ), - Column( - children: [ - for (var i = 0; i < model.programList.length - 1; i++) - StudentProgram(model.programList[i]), - ], - ), - const SizedBox(height: 10.0), if (model.isBusy) buildLoading(isInteractionLimitedWhileLoading: false) else - const SizedBox() + buildPage(context, model) ], ), ), @@ -141,7 +68,86 @@ class _ProfileViewState extends State { ); } +Widget buildPage(BuildContext context, ProfileViewModel model) => Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(5.0, 10.0, 5.0, 5.0), + child: SizedBox( + height: 90, + child: getMainInfoCard(model), + ), + ), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 8.0), + child: getMyInfosCard(model, context), + ), + Padding( + padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 4.0), + child: getMyBalanceCard(model, context), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 4.0), + child: getProgramCompletion(model, context), + ), + ], + ), + ), + ], + ), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + getCurrentProgramTile(model.programList, context), + const Divider( + thickness: 2, + indent: 10, + endIndent: 10, + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 16.0, top: 8.0, bottom: 8.0), + child: Text( + AppIntl.of(context).profile_other_programs, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: AppTheme.etsLightRed), + ), + ), + ], + ), + Column( + children: [ + for (var i = 0; i < model.programList.length - 1; i++) + StudentProgram(model.programList[i]), + ], + ), + const SizedBox(height: 10.0), + ], + ); + Card getMainInfoCard(ProfileViewModel model) { + var programName = ""; + if (model.programList.isNotEmpty) { + programName = model.programList.last.name; + } + return Card( child: Center( child: Column( @@ -157,11 +163,11 @@ Card getMainInfoCard(ProfileViewModel model) { ), ), ), - const Padding( - padding: EdgeInsets.all(5.0), + Padding( + padding: const EdgeInsets.all(5.0), child: Text( - 'Génie logiciel', - style: TextStyle( + programName, + style: const TextStyle( fontSize: 16, ), ), From 2e97eeb8892cbd1a93f690a467105f039448dc11 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Mon, 4 Sep 2023 15:45:07 -0400 Subject: [PATCH 07/19] Profile view test --- lib/ui/views/profile_view.dart | 3 -- test/ui/views/profile_view_test.dart | 73 ++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 9c1f9316f..a37409905 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -6,7 +6,6 @@ import 'package:notredame/ui/utils/app_theme.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:percent_indicator/percent_indicator.dart'; -import 'package:logger/logger.dart'; // VIEW-MODEL import 'package:notredame/core/viewmodels/profile_viewmodel.dart'; @@ -37,8 +36,6 @@ class ProfileView extends StatefulWidget { class _ProfileViewState extends State { final AnalyticsService _analyticsService = locator(); - final Logger _logger = locator(); - @override void initState() { super.initState(); diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index 0ecab1453..8aaa6798b 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -1,5 +1,7 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:io'; +import 'dart:math'; +import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -22,7 +24,27 @@ import '../../mock/managers/user_repository_mock.dart'; void main() { AppIntl intl; UserRepository userRepository; - + + final profileStudent = ProfileStudent( + firstName: "John", + lastName: "Doe", + permanentCode: "ABC123", + balance: "123456789"); + + // Make a test program object + final program = Program( + name: "Program name", + code: "1234", + average: "4.20", + accumulatedCredits: "123", + registeredCredits: "123", + completedCourses: "123", + failedCourses: "123", + equivalentCourses: "123", + status: "Actif"); + + final programList = [program]; + group('Profile view - ', () { setUp(() async { intl = await setupAppIntl(); @@ -30,9 +52,17 @@ void main() { userRepository = setupUserRepositoryMock(); setupAnalyticsServiceMock(); - UserRepositoryMock.stubGetInfo(userRepository as UserRepositoryMock); + UserRepositoryMock.stubGetInfo(userRepository as UserRepositoryMock, + toReturn: profileStudent); + UserRepositoryMock.stubProfileStudent( + userRepository as UserRepositoryMock, + toReturn: profileStudent); + + UserRepositoryMock.stubGetPrograms(userRepository as UserRepositoryMock, + toReturn: programList); - UserRepositoryMock.stubGetPrograms(userRepository as UserRepositoryMock); + UserRepositoryMock.stubPrograms(userRepository as UserRepositoryMock, + toReturn: programList); }); tearDown(() { @@ -40,37 +70,42 @@ void main() { unregister(); }); - testWidgets('contains student status', (WidgetTester tester) async { + testWidgets('contains main info', (WidgetTester tester) async { await tester.pumpWidget(localizedWidget(child: ProfileView())); await tester.pumpAndSettle(); - expect(find.widgetWithText(ListTile, intl.profile_student_status_title), + expect( + find.text("${profileStudent.firstName} ${profileStudent.lastName}"), findsOneWidget); - expect( - find.widgetWithText(ListTile, intl.profile_balance), findsOneWidget); + expect(find.text(program.name), findsNWidgets(2)); }); testWidgets('contains personal info', (WidgetTester tester) async { await tester.pumpWidget(localizedWidget(child: ProfileView())); await tester.pumpAndSettle(); - expect( - find.widgetWithText( - ListTile, intl.profile_personal_information_title), - findsOneWidget); + expect(find.text(profileStudent.permanentCode), findsOneWidget); - expect(find.widgetWithText(ListTile, intl.profile_first_name), - findsOneWidget); + expect(find.text(intl.profile_permanent_code), findsOneWidget); - expect(find.widgetWithText(ListTile, intl.profile_last_name), - findsOneWidget); + expect(find.text(intl.login_prompt_universal_code), findsOneWidget); + }); - expect(find.widgetWithText(ListTile, intl.profile_permanent_code), - findsOneWidget); + testWidgets('contains balance info', (WidgetTester tester) async { + await tester.pumpWidget(localizedWidget(child: ProfileView())); + await tester.pumpAndSettle(); - expect(find.widgetWithText(ListTile, intl.login_prompt_universal_code), - findsOneWidget); + expect(find.text(profileStudent.balance), findsOneWidget); + + expect(find.text(intl.profile_balance), findsOneWidget); + }); + + testWidgets('contains program completion', (WidgetTester tester) async { + await tester.pumpWidget(localizedWidget(child: ProfileView())); + await tester.pumpAndSettle(); + + expect(find.text(intl.profile_program_completion), findsOneWidget); }); group("golden - ", () { From f2ff7be369f355ec5a8cc27f2c7da79e596aa756 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Mon, 4 Sep 2023 16:35:06 -0400 Subject: [PATCH 08/19] Student program test --- test/ui/views/profile_view_test.dart | 1 - test/ui/widgets/student_program_test.dart | 62 +++++++++-------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index 8aaa6798b..7edad5322 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -1,6 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:io'; -import 'dart:math'; import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/ui/widgets/student_program_test.dart b/test/ui/widgets/student_program_test.dart index a3070ba53..609292a43 100644 --- a/test/ui/widgets/student_program_test.dart +++ b/test/ui/widgets/student_program_test.dart @@ -1,4 +1,6 @@ // FLUTTER / DART / THIRD-PARTIES +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -33,60 +35,40 @@ void main() { tearDown(() {}); group('has all categories - ', () { - testWidgets('contains one divider and one column', - (WidgetTester tester) async { + testWidgets('contains one expansion tile', (WidgetTester tester) async { await tester .pumpWidget(localizedWidget(child: StudentProgram(_program))); await tester.pumpAndSettle(); - final divider = find.byType(Divider); - expect(divider, findsNWidgets(1)); + expect(find.byType(ExpansionTile), findsOneWidget); - final column = find.byType(Column); - expect(column, findsNWidgets(1)); + expect(find.text(_program.name), findsOneWidget); }); - testWidgets('contains 9 listTile', (WidgetTester tester) async { + testWidgets('contains infos', (WidgetTester tester) async { await tester .pumpWidget(localizedWidget(child: StudentProgram(_program))); await tester.pumpAndSettle(); - final listTiles = find.byType(ListTile); - expect(listTiles, findsNWidgets(9)); - - expect(find.widgetWithText(ListTile, intl.profile_code_program), - findsOneWidget); - - expect(find.widgetWithText(ListTile, intl.profile_average_program), - findsOneWidget); - - expect( - find.widgetWithText( - ListTile, intl.profile_number_accumulated_credits_program), - findsOneWidget); + // Find the ExpansionTile and tap it to expand + final expansionTile = find.byType(ExpansionTile); + expect(expansionTile, findsOneWidget); + await tester.tap(expansionTile); + await tester.pumpAndSettle(); - expect( - find.widgetWithText( - ListTile, intl.profile_number_registered_credits_program), + expect(find.text(intl.profile_code_program), findsOneWidget); + expect(find.text(intl.profile_average_program), findsOneWidget); + expect(find.text(intl.profile_number_accumulated_credits_program), findsOneWidget); - - expect( - find.widgetWithText( - ListTile, intl.profile_number_completed_courses_program), + expect(find.text(intl.profile_number_registered_credits_program), findsOneWidget); - - expect( - find.widgetWithText( - ListTile, intl.profile_number_failed_courses_program), + expect(find.text(intl.profile_number_completed_courses_program), findsOneWidget); - - expect( - find.widgetWithText( - ListTile, intl.profile_number_equivalent_courses_program), + expect(find.text(intl.profile_number_failed_courses_program), findsOneWidget); - - expect(find.widgetWithText(ListTile, intl.profile_status_program), + expect(find.text(intl.profile_number_equivalent_courses_program), findsOneWidget); + expect(find.text(intl.profile_status_program), findsOneWidget); }); testWidgets('contains 17 Text fields', (WidgetTester tester) async { @@ -94,6 +76,12 @@ void main() { .pumpWidget(localizedWidget(child: StudentProgram(_program))); await tester.pumpAndSettle(); + // Find the ExpansionTile and tap it to expand + final expansionTile = find.byType(ExpansionTile); + expect(expansionTile, findsOneWidget); + await tester.tap(expansionTile); + await tester.pumpAndSettle(); + final text = find.byType(Text); expect(text, findsNWidgets(17)); }); From a0993bcdf50e48170f62be0f0e9f0a8f69ee9534 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Mon, 4 Sep 2023 16:46:55 -0400 Subject: [PATCH 09/19] ViewModel test --- test/viewmodels/profile_viewmodel_test.dart | 78 +++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/viewmodels/profile_viewmodel_test.dart b/test/viewmodels/profile_viewmodel_test.dart index 352bfcbd4..430eeb08f 100644 --- a/test/viewmodels/profile_viewmodel_test.dart +++ b/test/viewmodels/profile_viewmodel_test.dart @@ -1,6 +1,7 @@ // FLUTTER / DART / THIRD-PARTIES import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; +import 'package:notredame/core/constants/programs_credits.dart'; // MANAGERS import 'package:notredame/core/managers/settings_manager.dart'; @@ -67,6 +68,7 @@ void main() { setUp(() async { // Setting up mocks userRepository = setupUserRepositoryMock(); + setupAnalyticsServiceMock(); viewModel = ProfileViewModel(intl: await setupAppIntl()); }); @@ -147,6 +149,82 @@ void main() { }); }); + group("programProgression - ", () { + test("calculates program progression correctly", () { + // Create a list of programs for testing + final List testPrograms = [ + Program( + name: 'Program A', + code: '7625', // Program code with matching entry in ProgramCredits + average: '3.50', + accumulatedCredits: '30', + registeredCredits: '60', + completedCourses: '10', + failedCourses: '0', + equivalentCourses: '0', + status: 'Actif', + ), + Program( + name: 'Program B', + code: '7694', // Program code with matching entry in ProgramCredits + average: '3.20', + accumulatedCredits: '45', + registeredCredits: '90', + completedCourses: '20', + failedCourses: '5', + equivalentCourses: '0', + status: 'Actif', + ), + ]; + + UserRepositoryMock.stubPrograms(userRepository as UserRepositoryMock, + toReturn: testPrograms); + + // Create an instance of ProgramCredits + final ProgramCredits programCredits = ProgramCredits(); + + // Calculate the program progression + final double progression = viewModel.programProgression; + + // Calculate the expected progression based on the defined ProgramCredits + final double expectedProgression = + (45 / programCredits.programsCredits['7694'] * 100).roundToDouble(); + + // Verify that the calculated progression matches the expected value + expect(progression, expectedProgression); + }); + + test("handles no matching program code", () { + // Create a list of programs with no matching program code + final List testPrograms = [ + Program( + name: 'Program X', + code: + '9999', // Program code with no matching entry in ProgramCredits + average: '3.00', + accumulatedCredits: '20', + registeredCredits: '40', + completedCourses: '5', + failedCourses: '2', + equivalentCourses: '0', + status: 'Actif', + ), + ]; + + UserRepositoryMock.stubPrograms(userRepository as UserRepositoryMock, + toReturn: testPrograms); + + // Create an instance of ProgramCredits + final ProgramCredits programCredits = ProgramCredits(); + + // Calculate the program progression + final double progression = viewModel.programProgression; + + // The expected progression should be 0 when there is no matching program code + expect(progression, 0.0); + }); + }); + group('refresh -', () { test('Call SignetsAPI to get the user info and programs', () async { UserRepositoryMock.stubProfileStudent( From dad3287c0e125804ab2e9a8569e7f60b002174e1 Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 4 Sep 2023 19:06:30 -0400 Subject: [PATCH 10/19] Fix imports --- lib/ui/views/profile_view.dart | 2 +- lib/ui/widgets/student_program.dart | 4 ++-- test/ui/views/profile_view_test.dart | 4 +++- test/ui/widgets/student_program_test.dart | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index a37409905..56b053534 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -2,7 +2,6 @@ import 'dart:developer'; import 'package:flutter/material.dart'; -import 'package:notredame/ui/utils/app_theme.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:percent_indicator/percent_indicator.dart'; @@ -21,6 +20,7 @@ import 'package:notredame/ui/widgets/student_program.dart'; // UTILS import 'package:notredame/ui/utils/loading.dart'; +import 'package:notredame/ui/utils/app_theme.dart'; // CONSTANTS import 'package:notredame/core/constants/programs_credits.dart'; diff --git a/lib/ui/widgets/student_program.dart b/lib/ui/widgets/student_program.dart index 577be5d0d..648ff74d8 100644 --- a/lib/ui/widgets/student_program.dart +++ b/lib/ui/widgets/student_program.dart @@ -1,12 +1,12 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -// MODELS +// MODEL import 'package:ets_api_clients/models.dart'; // OTHER -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:notredame/ui/utils/app_theme.dart'; class StudentProgram extends StatefulWidget { diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index 7edad5322..a2a0f267e 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -1,10 +1,12 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:io'; -import 'package:ets_api_clients/models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +// MODELS +import 'package:ets_api_clients/models.dart'; + // MANAGERS import 'package:notredame/core/managers/user_repository.dart'; diff --git a/test/ui/widgets/student_program_test.dart b/test/ui/widgets/student_program_test.dart index 609292a43..600609275 100644 --- a/test/ui/widgets/student_program_test.dart +++ b/test/ui/widgets/student_program_test.dart @@ -1,6 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; From c9ee74cf5f9632d12b78af7ddb2965094854bbed Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 4 Sep 2023 19:09:30 -0400 Subject: [PATCH 11/19] Imports --- lib/ui/views/profile_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 56b053534..7b6414cfb 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -1,6 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; From 091039fe9689f78d207ab0fdd542f7bd8a7ea1a7 Mon Sep 17 00:00:00 2001 From: camillebrulotte Date: Mon, 4 Sep 2023 23:16:16 +0000 Subject: [PATCH 12/19] [BOT] Applying version. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3e45eb808..6a345a332 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.23.1+1 +version: 4.24.0+1 environment: sdk: ">=2.10.0 <3.0.0" From 589a17991d6e4684742cc707a7b11dc49e888270 Mon Sep 17 00:00:00 2001 From: clubapplets-server Date: Mon, 4 Sep 2023 23:18:23 +0000 Subject: [PATCH 13/19] [BOT] Applying format. --- lib/core/constants/programs_credits.dart | 38 ++++++++++++------------ lib/ui/widgets/student_program.dart | 9 +++--- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/lib/core/constants/programs_credits.dart b/lib/core/constants/programs_credits.dart index 4e2594c4b..186b4aed7 100644 --- a/lib/core/constants/programs_credits.dart +++ b/lib/core/constants/programs_credits.dart @@ -1,25 +1,25 @@ class ProgramCredits { Map programsCredits = { - "7625" : 117, - "7694" : 115, - "7084" : 116, - "7684" : 115, - "6556" : 117, - "6557" : 118, - "7086" : 116, - // TODO: Ajouter le code lorsque le programme sera en vigeur + "7625": 117, + "7694": 115, + "7084": 116, + "7684": 115, + "6556": 117, + "6557": 118, + "7086": 116, + // TODO: Ajouter le code lorsque le programme sera en vigeur // (https://www.etsmtl.ca/etudes/premier-cycle/Baccalaureat-informatique-distribuee) // "À venir" : 90", - "5766" : 30, - "4567" : 30, - "4412" : 30, - "4563" : 30, - "4684" : 30, - "4329" : 30, - "4288" : 30, - "Maîtrise" : 45, - "Programme court" : 15, - "Certificat" : 30, - "DESS" : 30, + "5766": 30, + "4567": 30, + "4412": 30, + "4563": 30, + "4684": 30, + "4329": 30, + "4288": 30, + "Maîtrise": 45, + "Programme court": 15, + "Certificat": 30, + "DESS": 30, }; } diff --git a/lib/ui/widgets/student_program.dart b/lib/ui/widgets/student_program.dart index 648ff74d8..6c19f01c0 100644 --- a/lib/ui/widgets/student_program.dart +++ b/lib/ui/widgets/student_program.dart @@ -18,7 +18,7 @@ class StudentProgram extends StatefulWidget { } class _StudentProgramState extends State - with TickerProviderStateMixin { + with TickerProviderStateMixin { bool showProgramDetails = false; AnimationController controller; Animation rotateAnimation; @@ -46,8 +46,7 @@ class _StudentProgramState extends State @override Widget build(BuildContext context) { - final bool isLightMode = - Theme.of(context).brightness == Brightness.light; + final bool isLightMode = Theme.of(context).brightness == Brightness.light; final List dataTitles = [ AppIntl.of(context).profile_code_program, @@ -75,7 +74,9 @@ class _StudentProgramState extends State data: Theme.of(context).copyWith( dividerColor: Colors.transparent, unselectedWidgetColor: Colors.red, - colorScheme: ColorScheme.fromSwatch().copyWith(onSecondary: Colors.red,), + colorScheme: ColorScheme.fromSwatch().copyWith( + onSecondary: Colors.red, + ), ), child: ExpansionTile( onExpansionChanged: (value) { From 67da9062250b443086b8477e9d98529210179080 Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 4 Sep 2023 19:53:03 -0400 Subject: [PATCH 14/19] Clean imports --- lib/ui/views/profile_view.dart | 4 ---- test/ui/widgets/student_program_test.dart | 1 - test/viewmodels/profile_viewmodel_test.dart | 3 --- 3 files changed, 8 deletions(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index 7b6414cfb..fee0c2e0a 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -1,5 +1,4 @@ // FLUTTER / DART / THIRD-PARTIES -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:stacked/stacked.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -21,9 +20,6 @@ import 'package:notredame/ui/widgets/student_program.dart'; import 'package:notredame/ui/utils/loading.dart'; import 'package:notredame/ui/utils/app_theme.dart'; -// CONSTANTS -import 'package:notredame/core/constants/programs_credits.dart'; - // OTHER import 'package:notredame/locator.dart'; diff --git a/test/ui/widgets/student_program_test.dart b/test/ui/widgets/student_program_test.dart index 600609275..1a77f8767 100644 --- a/test/ui/widgets/student_program_test.dart +++ b/test/ui/widgets/student_program_test.dart @@ -1,5 +1,4 @@ // FLUTTER / DART / THIRD-PARTIES -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/test/viewmodels/profile_viewmodel_test.dart b/test/viewmodels/profile_viewmodel_test.dart index 430eeb08f..0d5e4a06e 100644 --- a/test/viewmodels/profile_viewmodel_test.dart +++ b/test/viewmodels/profile_viewmodel_test.dart @@ -214,9 +214,6 @@ void main() { UserRepositoryMock.stubPrograms(userRepository as UserRepositoryMock, toReturn: testPrograms); - // Create an instance of ProgramCredits - final ProgramCredits programCredits = ProgramCredits(); - // Calculate the program progression final double progression = viewModel.programProgression; From 7e26e5ca9478139fd05bf64ffef70c3a73a9f861 Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Mon, 4 Sep 2023 20:21:24 -0400 Subject: [PATCH 15/19] [CI UPDATE GOLDENS] --- lib/core/managers/course_repository.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/managers/course_repository.dart b/lib/core/managers/course_repository.dart index e1a02767e..f3182a02a 100644 --- a/lib/core/managers/course_repository.dart +++ b/lib/core/managers/course_repository.dart @@ -1,6 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:logger/logger.dart'; From 8ab0155aafa5a8c0f72749311a6e6caadf64b190 Mon Sep 17 00:00:00 2001 From: Antoine Martineau Date: Mon, 4 Sep 2023 20:47:53 -0400 Subject: [PATCH 16/19] Added padding to the loading --- lib/ui/views/profile_view.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/ui/views/profile_view.dart b/lib/ui/views/profile_view.dart index fee0c2e0a..c90eb2912 100644 --- a/lib/ui/views/profile_view.dart +++ b/lib/ui/views/profile_view.dart @@ -49,7 +49,11 @@ class _ProfileViewState extends State { child: Column( children: [ if (model.isBusy) - buildLoading(isInteractionLimitedWhileLoading: false) + Padding( + padding: const EdgeInsets.only(top: 10.0), + child: + buildLoading(isInteractionLimitedWhileLoading: false), + ) else buildPage(context, model) ], From 73d01d5e86a006ed47afd1fb9a2f9912e06112ed Mon Sep 17 00:00:00 2001 From: Camille Brulotte Date: Thu, 7 Sep 2023 08:52:04 -0400 Subject: [PATCH 17/19] Update goldens --- test/ui/views/goldenFiles/profileView_1.png | Bin 1779 -> 8016 bytes test/ui/views/profile_view_test.dart | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/test/ui/views/goldenFiles/profileView_1.png b/test/ui/views/goldenFiles/profileView_1.png index 602e818087a56f94a7f5b8c24fb41b1e5a1a7479..de2b491212add7807d1c26029865636f4918466f 100644 GIT binary patch literal 8016 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 literal 1779 zcmeAS@N?(olHy`uVBq!ia0y~yVB`kkYaDDqk%X^n76U29;vjb?hIQv;UIIBR#ZI0f z96(URky;G-!WxGYS+^qJd?RqU#>FNogw z!FK+U?2kAfJvNmOh8O!RpMPWdAfDXwx_@yygIse2LnrCzWa@jy`1kVLXEZnO`SWY% z>)QYSBdY)3-@hS}1k+B=D9&@|XS}oB@YvNfMwKMGqv?Cubxs#`TUwmhwa&f&K=UAR z{q$cCo(saG+K?IVM-kL5XIl z=w|hSO*-3WJl=Ax^zPSxH~0U)_uf67BtIHHztbnjv?D*cXZ2Yok0W$Pde!_9)1HbI z+pH;#E#Le8_uJofHS&Wxu$w1GUH|$*up@_*Xx;SNu6uK_Jac;DfW{6iv?~=1+ znn+TVm)%b Date: Thu, 7 Sep 2023 09:23:01 -0400 Subject: [PATCH 18/19] Add missing import --- test/ui/views/profile_view_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ui/views/profile_view_test.dart b/test/ui/views/profile_view_test.dart index 89fb0dfc8..a8022a9a7 100644 --- a/test/ui/views/profile_view_test.dart +++ b/test/ui/views/profile_view_test.dart @@ -1,4 +1,5 @@ // FLUTTER / DART / THIRD-PARTIES +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; From 154c562e153ee2a5de9f2af273b277aca60cdd34 Mon Sep 17 00:00:00 2001 From: camillebrulotte Date: Thu, 7 Sep 2023 13:28:54 +0000 Subject: [PATCH 19/19] [BOT] Update golden files --- test/ui/views/goldenFiles/profileView_1.png | Bin 8016 -> 6838 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/ui/views/goldenFiles/profileView_1.png b/test/ui/views/goldenFiles/profileView_1.png index de2b491212add7807d1c26029865636f4918466f..ce85ded5d9e7abeae67ba9bb11de004ce11557b7 100644 GIT binary patch literal 6838 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 az_Oae(Ep)|gLhAm<40|elpeb9^Zx=gUT(1f literal 8016 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